GraphQL API
Vectis exposes a single GraphQL endpoint at /graphql powered by Strawberry (code-first, type-safe). All queries and mutations are composed from module resolvers via multiple inheritance in vectis/core/graphql.py.
Endpoint
| Environment | URL |
| Development | http://localhost:8000/graphql |
| Via BFF proxy | http://localhost:5174/api/graphql (storefront) or http://localhost:5173/api/graphql (admin) |
The Strawberry GraphQL IDE is available at the development URL in the browser.
Authentication
All requests go through the BFF layer. The SvelteKit server attaches headers:
| Header | Source | Purpose |
Authorization | Bearer {JWT} from Redis session | Authenticated user identity |
X-Session-ID | vectis_cart_session cookie | Guest cart tracking |
X-Channel-Slug | /c/{slug}/* route prefix or vectis_channel_slug cookie | URL-driven channel context (Decided #164) |
X-Channel-ID | Resolved by middleware | Channel context (optional, falls back when slug is absent) |
X-Forwarded-For | BFF passes the client IP for audit + rate-limit attribution | Real client IP behind the BFF/reverse proxy (Decided #165) |
X-Locale | vectis_locale cookie | Content translation language |
X-Currency | vectis_currency cookie | Price display currency |
Key Queries
| Query | Description |
channelInfo | Channel configuration: commerce mode, supported languages/currencies |
me | Current authenticated user |
products(limit, offset) | Product catalog with variants |
product(id) | Single product with full variant/pricing data |
resolvePrice(variantId, quantity) | Resolved price for a variant respecting hierarchy + currency |
myCart | Current user/session cart with enriched lines |
order(id) / orders(...) | Order lookup with lines, tax, shipping, fraud status |
accountCommerceSummary(accountId, currency) | B2B account rollup: order count, revenue total, average order value, last order time. Counts and sums use orders whose status is not Cancelled or Refunded (same currency label as currency; amounts are from stored order grand_total). |
accounts(...) | Paginated B2B accounts with filters: search (company, legal name, owner email), status, creditHoldStatus, customerGroupId, taxExempt, currency, riskLevel (use __unset__ for accounts with no risk level), createdFrom / createdTo, updatedFrom / updatedTo, sortBy, sortDir, limit, offset. Returns updatedAt, customerGroup, taxId, limits, etc. |
customerGroups | All customer groups (name, slug) for admin filters and display |
customerGroup(id) | Single customer group by ID (for edit form) |
accountAdminNotes(accountId, limit, offset, includeArchived) | Staff-only notes on a B2B account. Returns { notes, total }. Requires account.update. |
quotes(accountId, status, limit, offset) | Quote list; returns { quotes, total } (not a bare array) |
exchangeRates(baseCurrency) | Active exchange rates |
discountRules(enabledOnly) | Promotion rules |
savedPaymentMethods(accountId, locationId) | Saved cards for an account, optionally scoped by location |
paymentMethods(channelId) | Configured payment gateways |
paymentTransactions(orderId) | Transaction ledger for an order |
auditLog(entityType, entityId, limit) | Audit trail entries (includes createdAt); when authenticated: requires audit.view. Ordered by created_at descending. |
notificationTemplates | Registered email templates: code, name, module, subjectTemplate, bodyTemplate, enabled, variables (JSON) |
notificationLogs(limit) | Recent delivery attempts (template code, recipient, channel, status) |
warehouses(search, activeOnly) | Warehouse list with optional search/filter; requires inventory.view |
stockLevels(warehouseId, productId, search, sortBy, sortDir, limit, offset) | Paginated stock levels enriched with SKU/product/variant names; requires inventory.view |
stockLevelsForProduct(productId, warehouseId) | All variant stock levels for a product (bulk editor); requires inventory.view |
stockAdjustmentLog(variantId, warehouseId, limit, offset) | Paginated audit trail with user email; requires inventory.view |
savedReports | Saved report definitions (SavedReport): name, reportType, JSON config, visibility (Decided #82) |
storefrontSearchConfig | Typesense host, collection, scoped browser key, plus autocomplete flags and limits (autocompleteEnabled, autocompleteMinChars, autocompleteDebounceMs, autocompleteMaxResults) loaded from search.% settings (Decided #105) |
Key Mutations
| Mutation | Description |
login(email, password) | Authenticate and receive access token |
registerCustomer(...) | B2C customer registration |
addToCart(input) | Add variant to cart with price resolution |
applyCoupon(code) | Apply coupon code to cart |
removeCoupon(code) | Remove coupon from cart |
checkout(input) | Full checkout: promotions, shipping, tax, payment, order creation |
updateOrderStatus(orderId, status) | Transition order state (triggers auto void/refund on cancel) |
modifyOrder(orderId, ...) | Modify an authorized order: add/remove/update lines with reauthorization |
adminCreateOrder(...) | Staff: create order directly from line items with saved card charging |
adminReleaseFraudHold(orderId, transactionId) | Staff: release a transaction held by gateway fraud filters |
adminCaptureTransaction(transactionId, amount) | Staff: capture an authorized transaction |
adminVoidTransaction(transactionId) | Staff: void an authorized transaction |
adminRefundTransaction(transactionId, amount, lastFour) | Staff: refund a captured transaction |
adminChargeSavedCard(savedPaymentMethodId, amount, ...) | Staff: charge a customer's saved card |
addPaymentMethod(...) | Save a new card from Accept.js opaque data |
deletePaymentMethod(id) | Delete a saved card |
setDefaultPaymentMethod(id) | Set a saved card as default |
createPaymentMethod(...) | Admin: configure a payment gateway (requires settings.edit when authenticated) |
updatePaymentMethod(id, ...) | Admin: update gateway configuration (requires settings.edit when authenticated) |
setExchangeRate(input) | Admin: set currency exchange rate |
setAccountPrice(input) | Admin: set account-level price override |
createCustomerGroup(input) | Admin: create customer group. Requires settings.edit. |
updateCustomerGroup(id, input) | Admin: update customer group name/slug. Requires settings.edit. |
deleteCustomerGroup(id) | Admin: delete customer group (blocked if accounts/customers reference it). Requires settings.edit. |
updateAccount(accountId, input) | Admin: update account fields including customerGroupId. |
addAccountAdminNote(accountId, body, attachments?) | Admin: add staff-only note to account with optional image attachments (JSON array of {url, filename, content_type, size}). Requires account.update. |
updateAccountAdminNote(noteId, body?, attachments?) | Admin: edit a note's body and/or attachments. Authors can edit within 24 hours; super admins (* permission) can edit any note at any time. Returns AccountNoteType with updatedAt and attachments. Requires account.update. |
archiveAccountAdminNote(noteId) | Admin: archive a note. Requires account.update. |
toggleChannelExtension(...) | Admin: enable/disable extension per channel |
createProduct(input) | When authenticated: requires product.create permission |
updateProduct(input) | When authenticated: requires product.update permission |
createVariant(input) | When authenticated: requires product.create permission |
createTaxCategory / createTaxRate / updateTaxRate / deleteTaxRate | When authenticated: requires settings.edit |
createPage / updatePageBlocks | When authenticated: requires settings.edit |
createSearchSynonym | When authenticated: requires settings.edit |
registerNotificationTemplate / updateNotificationTemplate / sendNotification | When authenticated: requires settings.edit |
updateNotificationPreference | When authenticated: requires settings.edit; upserts NotificationPreference for a user and category |
notificationPreferences(userId) | When authenticated: requires settings.edit; lists preferences for the given user |
subscribeToRestock(variantId, email) | Restock alerts (Decided #84): signed-in uses userId from session; guests must pass email |
unsubscribeFromRestock(variantId) | Remove restock subscription for the signed-in user (guest email subscriptions require sign-in to manage via API) |
createSalesRep / assignSalesRep | When authenticated: requires settings.edit |
issueStoreCredit(amount, reason, accountId?, customerId?, notes?, locationId?) | Issue store credit to a B2B account or B2C customer. Optional locationId scopes to a specific location (B2B only). When authenticated: requires order.update. Records created_by from the authenticated user. |
cancelStoreCredit(transactionId, reason) | Cancel an active credit transaction. Creates a reversal and decrements the balance. Requires order.update. |
redeem_gift_card(code, accountId?, customerId?, userId?, amount?) | Redeem a gift card into store credit. Supports B2B (accountId) and B2C (customerId). Derives from session context if neither is provided. |
createTag(input: CreateTagInput) | Create a tag. Requires settings.edit. Input accepts name, color, and optional applicableEntityTypes: [String] to scope the tag. |
updateTag(id, input: UpdateTagInput) | Update tag name, color, or applicableEntityTypes. Requires settings.edit. Re-derives slug on name change; rejects duplicate slugs. |
deleteTag(id) | Delete tag and all its assignments. Requires settings.edit. |
assignTag(input: {tagId, entityType, entityId}) | Assign a tag to an entity. Requires {entity}.update permission for the given entityType (see Tagging section in conventions.md). assigned_by set from session. Enforces tag scoping: if applicableEntityTypes is set on the tag, the entityType must be in the list. |
unassignTag(tagId, entityType, entityId) | Remove tag from entity. Same permission gate as assignTag. |
TagType Fields
| Field | Type | Description |
id | ID | Tag identifier |
name | String | Tag display name |
slug | String | URL-safe slug auto-derived from name |
color | String? | Hex color for display |
applicableEntityTypes | [String]? | Entity types this tag can be assigned to. null means all types. |
Tag Queries
| Query | Args | Returns | Description |
tags | — | [TagType] | All tags (for admin Tags page) |
tagsForEntity | entityType, entityId | [TagType] | Tags assigned to a specific entity |
tagsForEntityType | entityType: String! | [TagType] | Tags applicable to a given entity type (for tag picker dropdowns). Returns tags where applicableEntityTypes is null OR contains the given type. |
All five entity types expose a tags: [TagType] field:
| Type | tags resolved via |
ProductType | Batch-loaded on products() list; single-load on product() detail |
OrderType | Batch-loaded on orders() list; single-load on order() detail |
AccountType | Batch-loaded on accounts() list; single-load on account() detail |
LocationType | Batch-loaded on account() detail (nested locations) |
EmployeeType | Batch-loaded on account() detail (nested employees) |
Tag Filtering on List Queries
| Query | New arg | Behavior |
products(tagIds: [Int!]) | tagIds | Filter products tagged with ANY of the provided tag IDs |
orders(tagIds: [Int!]) | tagIds | Filter orders tagged with ANY of the provided tag IDs |
accounts(tagIds: [Int!]) | tagIds | Filter accounts tagged with ANY of the provided tag IDs |
AI Content Generation
| Query / Mutation | Description |
aiProviders | List installed AI providers with health status |
aiPromptTemplates | List all prompt templates (stored DB rows merged with built-in defaults). Returns isBuiltin flag |
generateAiContent(useCase, context, providerName?, maxTokens?, temperature?) | Generate content using a prompt template. Falls back to built-in defaults when no stored template exists |
upsertAiPromptTemplate(useCase, template) | Create or update a prompt template |
deleteAiPromptTemplate(useCase) | Delete a stored template. For built-in use cases, reverts to the default prompt |
Inventory (Decided #113)
| Mutation | Description |
createWarehouse(input) | Create a new warehouse. Requires inventory.manage. |
updateWarehouse(warehouseId, input) | Update warehouse fields. Requires inventory.manage. |
deactivateWarehouse(warehouseId) | Soft-deactivate a warehouse. Requires inventory.manage. |
adjustStock(variantId, warehouseId, newQuantity, reason, notes) | Set stock to an absolute quantity, writes audit log. Requires inventory.manage. |
bulkAdjustStock(input) | Batch stock updates for multiple variants. Input includes items: [{variantId, warehouseId, newQuantity}], reason, notes. Requires inventory.manage. |
importInventory(items, notes) | Import stock from CSV data. Items: [{sku, warehouseCode, quantity}]. Matches by SKU + warehouse code. Requires inventory.manage. |
Reporting (Decided #82)
| Mutation | Description |
createSavedReport | Persist a named report with reportType and JSON config (e.g. date_from, date_to). |
runReport(reportId) | Load the saved report, execute ReportExecutionService, return rows as JSON. |
exportReport(reportId, format) | Same execution; returns CSV as a string when format is csv (other formats rejected until implemented). |
createScheduledReport | Create a ScheduledReport row (schedule string, recipients, format) linked to a SavedReport. |
updateScheduledReport | Update schedule and/or enabled on a scheduled report (id argument). |
Supported reportType values: sales_summary, product_performance, customer_orders, inventory_levels, tax_collected. Order-based reports honor config.date_from and config.date_to when present.
Payment gateway configuration mutations
createPaymentMethod, updatePaymentMethod, and deletePaymentMethod when removing a configured gateway (PaymentMethod row) require settings.edit when the caller is authenticated. (Customer saved-card deletion may use the same field name depending on schema composition.)
Fulfillment and shipping mutations
Shipping configuration mutations (createShippingZone, updateShippingZone, deleteShippingZone, setZoneMethods, provider/method/box create–update–delete) require the settings.edit permission when the caller is authenticated. Shipment lifecycle mutations (createShipment, shipShipment, deliverShipment) require order.update. The shippingRatesForCheckout query is intentionally unguarded so the storefront can resolve rates without staff permissions.
Store Credit API
Queries
| Query | Args | Returns | Description |
storeCreditBalance | accountId?, customerId?, locationId? | StoreCreditBalanceType | Balance and transactions for a specific ledger |
storeCreditSummary | accountId?, customerId? | StoreCreditSummaryType | All ledgers for an account or customer |
myStoreCredit | (none — uses session) | StoreCreditBalanceType | Caller's own credit (storefront use). Uses account_id or customer_id from session context. |
customers | search?, limit?, offset? | [CustomerType] | List B2C customers (admin use). Search by name or email. |
Types
StoreCreditBalanceType: accountId, customerId, locationId, locationName, balance (MoneyType), transactions ([CreditTransactionType]) StoreCreditSummaryType: accountId, customerId, ledgers ([StoreCreditBalanceType]) CreditTransactionType: id, amount (MoneyType), type, status, reason, referenceType, referenceId, notes, createdAt, createdByName
Checkout Integration
The CheckoutInput accepts storeCreditAmount (String, optional). At checkout the system:
- Caps the requested amount at min(requested, balance, grandTotal)
- Debits store credit before calling the payment gateway
- Charges the remainder to the gateway (or skips it if fully covered)
- On gateway failure, compensates by refunding the debited credit back
The OrderType exposes storeCreditApplied (MoneyType) showing how much credit was used.
The checkout mutation accepts a comprehensive input:
mutation Checkout($input: CheckoutInput!) {
checkout(input: $input) {
id
orderNumber
status
currency
paymentStatus
fraudStatus
subtotal { amount currency }
discountTotal { amount currency }
taxTotal { amount currency }
shippingTotal { amount currency }
grandTotal { amount currency }
lines {
sku
productName
quantity
lineTotal { amount currency }
taxTotal { amount currency }
discountAmount { amount currency }
}
}
}
Input fields:
| Field | Type | Description |
email | String | Required for guest checkout |
poNumber | String | B2B purchase order reference |
acceptPolicy | Boolean | Terms acceptance |
shippingAddress | AddressInput | Ship-to address |
billingAddress | AddressInput | Bill-to address |
shippingCarrier | String | Selected carrier code |
shippingMethodCode | String | Selected shipping method |
paymentMethod | String | Payment gateway code |
paymentData | JSON | Gateway-specific payload |
couponCodes | [String] | Additional coupon codes to apply at checkout |
savedPaymentMethodId | Int | Use a saved card (CIM profile) |
opaqueDataDescriptor | String | Accept.js token descriptor |
opaqueDataValue | String | Accept.js token value |
saveCard | Boolean | Save the card for future use after checkout |
storeCreditAmount | String | Amount of store credit to apply (Decimal as string). Capped at min(requested, balance, grandTotal). |
Order Type
The OrderType includes payment and fraud fields:
| Field | Type | Description |
paymentStatus | String | authorized, captured, held_for_review, voided, refunded, failed |
paymentCardBrand | String | Card network slug from the latest successful charge/authorization (visa, mastercard, amex, …) when the gateway stored it; null if unknown |
fraudStatus | String | null, held_for_review, released, declined |
fraudDetails | JSON | FDS filter names and details when flagged |
storeCreditApplied | MoneyType | Amount of store credit used on this order (split payment). 0 if none. |
Money Type
All monetary values use the MoneyType:
type MoneyType {
amount: String! # Decimal as string for precision
currency: String! # ISO 4217 code
}
Admin Cart API (Decided Cart Mgmt #2)
Admin cart operations require cart.view (queries) or cart.edit (mutations) permission.
Queries
| Query | Args | Returns | Description |
adminCarts | channelId, accountId, search, hasLines, sortBy, sortDir, limit, offset | AdminCartListResult | Paginated cart dashboard |
adminCart | id | AdminCartType | Full cart detail with enriched lines |
cartSnapshots | cartId, limit | [CartSnapshotType] | Snapshot timeline |
cartValidation | cartId | [CartValidationIssueType] | Non-blocking error checking |
cartAuditLog | cartId, limit | [CartAuditEntryType] | Audit trail for this cart |
Mutations
| Mutation | Args | Description |
adminCreateCart | channelId, accountId?, locationId?, employeeId?, customerId?, currency? | Create a new empty cart |
adminAddCartLine | cartId, variantId, quantity, managedPrice? | Add item with optional managed price |
adminUpdateCartLine | cartId, lineId, quantity | Change quantity |
adminRemoveCartLine | cartId, lineId | Remove item |
adminSetManagedPrice | cartId, lineId, price? | Set/clear managed price override |
adminApplyCoupon | cartId, code | Apply coupon code |
adminRemoveCoupon | cartId, code | Remove coupon code |
adminSetCartAddresses | cartId, shippingAddressId?, billingAddressId? | Update addresses |
adminSetCartNotes | cartId, notes | Set admin notes |
adminSetLineNotes | cartId, lineId, notes | Set per-line admin notes |
adminLockCart | cartId | Acquire editing lock (Decided #92) |
adminUnlockCart | cartId | Release editing lock |
adminRestoreCartSnapshot | cartId, snapshotId | Restore cart to snapshot state |
All mutations return AdminCartType with the updated cart state.
Registration (Decided #114)
Queries
| Query | Args | Returns | Purpose |
registrationForms | activeOnly?, channelId? | [RegistrationFormType] | List registration forms with steps/sections/fields |
registrationForm | id?, slug? | RegistrationFormType | Get form by ID or slug (or default) |
registrationRules | activeOnly? | [RegistrationRuleType] | List geo-conditional rules |
documentTypes | activeOnly? | [DocumentTypeType] | List document types |
agreementTemplates | activeOnly?, formId? | [AgreementTemplateType] | List agreement templates |
invitationCodes | activeOnly? | [InvitationCodeType] | List invitation codes |
registrationSubmissions | status?, formId?, limit, offset | SubmissionListResult | Paginated submissions |
registrationSubmission | id | RegistrationSubmissionType | Submission detail with docs/sigs |
evaluateRegistrationRules | country?, state?, county?, city?, postalCode?, formId? | RulesEvaluationType | Evaluate rules for a geo location |
accountDocuments | accountId | [DocumentUploadType] | Documents for a specific account |
Mutations
| Mutation | Args | Description |
createRegistrationForm | input: CreateRegistrationFormInput | Create a new registration form |
updateRegistrationForm | id, input: UpdateRegistrationFormInput | Update form metadata |
deleteRegistrationForm | id | Delete form and all nested structure |
saveFormStructure | formId, steps: [FormStructureStepInput] | Replace full step/section/field tree |
createRegistrationRule | input: CreateRuleInput | Create geo-conditional rule |
updateRegistrationRule | id, input: UpdateRuleInput | Update rule |
deleteRegistrationRule | id | Delete rule |
createDocumentType | input: CreateDocumentTypeInput | Create document type |
updateDocumentType | id, input: UpdateDocumentTypeInput | Update document type |
deleteDocumentType | id | Delete document type |
createAgreementTemplate | input: CreateAgreementTemplateInput | Create agreement |
updateAgreementTemplate | id, input: UpdateAgreementTemplateInput | Update agreement |
deleteAgreementTemplate | id | Delete agreement |
createInvitationCode | input: CreateInvitationCodeInput | Create invitation code |
updateInvitationCode | id, input: UpdateInvitationCodeInput | Update code |
deleteInvitationCode | id | Delete code |
reviewDocument | input: ReviewDocumentInput | Approve/reject uploaded document |
submitRegistration | input: SubmitRegistrationInput | Public: submit registration form |
reviewRegistration | input: ReviewSubmissionInput | Admin: approve/deny/request-docs |
Banner & Slider (Decided #116, #117)
Queries
| Query | Arguments | Description |
banners | activeOnly: Boolean | List all banners |
banner | id: ID! | Get single banner |
bannersByPlacement | placementType: String!, entitySlug: String | Get banners for a placement |
sliders | activeOnly: Boolean | List all sliders |
slider | id: ID! | Get single slider with slides |
Mutations
| Mutation | Arguments | Description |
createBanner | input: BannerInput! | Create banner |
updateBanner | id: ID!, input: BannerInput! | Update banner |
deleteBanner | id: ID! | Delete banner |
createSlider | input: SliderInput! | Create slider |
updateSlider | id: ID!, input: SliderInput! | Update slider |
deleteSlider | id: ID! | Delete slider and all slides |
addSlide | sliderId: ID!, input: SlideInput! | Add slide to slider |
updateSlide | id: ID!, input: SlideInput! | Update slide |
removeSlide | id: ID! | Remove slide |
reorderSlides | sliderId: ID!, slideIds: [ID!]! | Reorder slides |
Tracking & Attribution (Decided #118)
Queries
| Query | Arguments | Description |
trackingAnalytics | entityType, entityId?, dateFrom?, dateTo? | Aggregate analytics |
trackingEvents | entityType?, entityId?, dateFrom?, dateTo?, limit, offset | Paginated event log |
Mutations
| Mutation | Arguments | Description |
trackEvent | eventType, entityType, entityId, pageUrl?, referrerUrl? | Record tracking event (public) |
Email Campaigns (Decided #115)
Queries
| Query | Arguments | Description |
emailCampaigns | statusFilter?: String | List campaigns |
emailCampaign | id: ID! | Get single campaign |
Mutations
| Mutation | Arguments | Description |
createEmailCampaign | input: EmailCampaignInput! | Create campaign |
updateEmailCampaign | id: ID!, input: EmailCampaignInput! | Update campaign |
sendEmailCampaign | id: ID! | Send campaign |
cancelEmailCampaign | id: ID! | Cancel campaign |
deleteEmailCampaign | id: ID! | Delete campaign |
Packaging & Units of Measure (Decided #25, #235)
Products can ship a ladder of packages (each, pack-of-6, case-of-24) with a canonical unit-of-measure. The cart, order, and reorder paths preserve the chosen package per line.
Types
| Type | Description |
PackageType | Global package (slug, label, multiplier, active flag) |
ProductPackageType | Product → package mapping with sku, price overrides, eaches conversion |
Queries
| Query | Args | Description |
packages(activeOnly) | — | List global packages (admin picker) |
ProductType.packages | — | All packages enabled for a product (PDP grid) — nested field, no standalone query |
Mutations
| Mutation | Description |
createPackage(input: CreatePackageInput!) | Create a new global package. Requires settings.edit. |
updatePackage(id: ID!, input: UpdatePackageInput!) | Rename or change multiplier. |
deletePackage(id: ID!) | Remove a global package (rejected if referenced). |
setProductPackages(productId: ID!, input: SetProductPackagesInput!) | Replace the per-product package ladder atomically. Requires product.update. |
Cart lines carry unitPackageId and unitLabel; reorder respects the original package slug + multiplier and surfaces an unitChanged flag on the new cart line when the ladder has shifted (Decided #235).
Min/Max Per Customer (MMOQ — Decided #236)
Tracked product variants can declare per-order and 30-day per-customer caps. The cart aggregate enforces these caps; the storefront previews them on bulk-lookup rows.
Types
| Type | Description |
MmoqViolationDetail | Per-line violation detail |
Fields
ProductType and ProductVariantType carry maxBackorderQty, maxPerCustomerPerOrder, maxPerCustomer30d, and mmoqDisplayUnitId. MMOQ is enforced at the variant level on the DB (ProductVariant.*); the same fields are surfaced on ProductType so the storefront PDP can render limits without re-resolving per-variant.
CartBulkLookupRow.mmoqDetail: MmoqViolationDetail returns from cartBulkLookup so the storefront can preview violations before commit. MMOQ is re-evaluated at finalize time under a serializable transaction.
Mutations
| Mutation | Description |
clearProductMmoq(productId: ID!): Int! | Reset the running 30-day window for a product. Requires product.update. |
Inventory Lifecycle (Decided #160, #161, #234)
| Mutation | Description |
pushExternalStock(input: PushExternalStockInput!) | API-key-scoped: feed external stock counts. Tracked products update reservable inventory; untracked products only update Variant.external_stock_snapshot (audit only on the DB; not exposed in GraphQL). |
clearProductInventoryState(productId: ID!): ClearProductInventoryStateResult! | Bumps Product.inventory_state_version and releases the relevant HELD reservations. Requires inventory.manage. |
clearProductBackorders(productId: ID!) | Clears inventory_risk_flag and unblocks order resumption. Requires inventory.manage. |
acknowledgeOrderExternalHandoff(orderId: ID!, externalReference: String) | ERP-side ack: releases internal holds so the ERP becomes the system of record. Requires order.update. |
Fields exposed in GraphQL
ProductType.trackInventory, trackInventoryEffective, packagingEnabled, packagingAvailableForChannel ProductType.maxBackorderQty, maxPerCustomerPerOrder, maxPerCustomer30d, mmoqDisplayUnitId ProductType.labels: [ProductLabelInstanceType!]! OrderType.inventoryRiskFlag, externalHandoffAcknowledgedAt, externalHandoffReference, isReissue, reissuedFromOrderId, reissuedFromOrderNumber, reissueReason orders(inventoryRiskOnly: Boolean = false) — filter the order list to flagged orders only
The Product.inventory_state_version and Variant.external_stock_snapshot columns exist on the DB (for race-guard reads and audit) but are not currently exposed via GraphQL.
OrderLineItem.tracking_enabled_at_checkout (DB column) captures the tracking flag at order-place time so fulfillment isn't surprised by a live toggle.
Product Labels (Decided #228)
Predicate-driven product labels (e.g., "Sale", "Local", "Low Stock") evaluated in batches and rendered on PDP, related products, recently-viewed, search results, and the catalog cards.
Types
| Type | Description |
ProductLabelType | Label definition: name, slug, color, icon, predicate JSON, priority, active flag |
ProductLabelInstanceType | Resolved label on a specific product (with stale-reason if applicable) |
Queries
| Query | Args | Description |
productLabels(activeOnly) | — | List label definitions |
productLabel(id) | — | Single label |
Mutations
| Mutation | Description |
createProductLabel(input) | Create a new label with namespaced predicate. Requires product.update. |
updateProductLabel(id, input) | Update label / change predicate. |
deleteProductLabel(id) | Delete label and detach all instances. |
clearProductLabelStaleReferences(extensionName: String!): Int! | Garbage-collect resolved instances whose source predicate no longer matches. The extensionName argument scopes cleanup to one extension's predicates. |
ProductType.labels: [ProductLabelInstanceType!]! is batch-loaded on products() list and single-loaded on product() detail.
Cart Approval Workflow (B2B Rounds 1–3)
Multi-phase approval — buyers submit a cart, approvers move it through cart and payment phases. State and rejection audit live on Cart (cart_approved_grand_total, status='cart_approved_blocked_inventory') and on the CartRejectionEvent table — both DB-only; the workflow events surface through the GraphQL surface below.
Types
| Type | Description |
CartApprovalRequestType | Approval state (phase, requester, approver, timestamps, expires_at) |
CartApprovalResultType | Mutation result with refreshed cart state |
CartTenderType | Per-cart payment tender (amount, gateway, sequence, isRemainder) |
PaymentCascadeInstructionInput | Approver-curated tender cascade input for recurateCascade |
RecurateCascadeResult | Result envelope for per-order recurate |
UpdateRecurringCascadeResult | Result envelope for recurring-order cascade update |
Mutations (exact signatures)
| Mutation | Signature |
| Submit | submitCartForApproval(cartId: ID!, phase: String!, approverEmployeeId: Int = null, idempotencyKey: String = null): CartApprovalResultType! |
| Approve cart phase | approveCartPhase(cartId: ID!): CartApprovalResultType! |
| Reject cart phase | rejectCartPhase(cartId: ID!, reason: String!): CartApprovalResultType! |
| Withdraw cart | withdrawCartApproval(cartId: ID!): Boolean! |
| Select approver | selectApproverForPendingCart(cartId: ID!, approverEmployeeId: Int!): CartApprovalRequestType! |
| Cancel selection | cancelPendingApproverSelection(cartId: ID!): Boolean! |
| Add tender | addCartTender(cartId: ID!, paymentMethodId: Int!, sequence: Int!, amount: String = null, isRemainder: Boolean! = false): CartTenderType! |
| Remove tender | removeCartTender(id: ID!): Boolean! |
| Approve payment phase | approvePaymentPhase(cartId: ID!, paymentCascade: [PaymentCascadeInstructionInput!]!, idempotencyKey: String = null): CartApprovalResultType! |
| Reject payment phase | rejectPaymentPhase(cartId: ID!, reason: String!): CartApprovalResultType! |
| Withdraw payment | withdrawPaymentApproval(cartId: ID!): Boolean! |
| Recurate cascade | recurateCascade(orderId: ID!, newCascade: JSON!, reason: String = null): RecurateCascadeResult! |
| Recurring cascade | updateRecurringOrderCascade(recurringId: Int!, cascade: JSON!): UpdateRecurringCascadeResult! |
| Admin bypass | adminBypassApproval(cartId, reason) — requires cart.edit + cart.bypass_approval |
approvePaymentPhase requires the approver to pass the full cascade (which gateways try in which order); this is the contract approval gate enforces, not an "approve last-submitted cascade" shortcut.
AdminChannelSettingsType exposes approvalMode, defaultCartApproverEmployeeId, defaultPaymentApproverEmployeeId, defaultOrderApproverEmployeeId, and cartApprovalInvalidatesOnEdit.
Refund Approval (OQ #26 / #27)
Refunds above thresholds enter a durable Temporal RefundExecutionWorkflow with per-tender progress tracking. Self-approval is blocked at the API.
| Type | Description |
RefundRequestType | id, status, requester, approver, per-tender progress, requestedAt/decidedAt/executedAt |
| Mutation | Signature |
submitRefundForApproval(orderId, input): RefundRequestType! | Request a refund. Requires order.refund. |
decideRefundApproval(refundRequestId: ID!, approved: Boolean!, notes: String = null): RefundRequestType! | Approver decision. Requires order.refund.approve. Self-approval blocked. |
retryRefundExecution(refundRequestId: ID!): RefundRequestType! | Retry a refund that failed at execution time. |
The admin "Refund Approval Inbox" page renders the request queue using these queries and mutations.
Quick Order Lists
| Type | Description |
ListType, ListItemType, ListShareType | Saved order lists with per-user sharing and list → cart conversion |
| Mutation | Description |
createList(input) / updateList(id, input) | Manage saved lists. |
addListItem / removeListItem | Edit list contents. |
shareList / unshareList / transferList / reassignList | Sharing and ownership. |
sendList | Email a list to other employees. |
convertListToCart(listId, locationId) | Convert a saved list to a cart, honouring MMOQ + per-product package selection. |
Cart Bulk Operations (Decided #229, #230, #231)
| Mutation | Signature |
| Bulk add | bulkAddToCart(input) — partial-success; returns rejected rows |
| Bulk remove | bulkRemoveCartLines(lineIds: [ID!]!): CartType (no cartId — lines carry the cart ref) |
| Save for later | bulkSaveCartLinesForLater(lineIds: [Int!]!): [SavedCartItemType!]! |
| Restore | restoreSavedCartLine(savedItemId) |
CartBulkLookupResult returns { rows: [CartBulkLookupRow] } with per-row candidates, mmoqDetail, unitChanged, and errors. Used by the Quick Order UI to preview matches before commit.
Cart AI (Decided #232)
| Type | Description |
CartAIParseInput | Parse request: free-text or image URL |
CartAIParseResult | Parsed line candidates with confidence + error taxonomy |
| Operation | Signature |
cartAiParseToLookupItems(input: CartAIParseInput!): CartAIParseResult! | Parse free-text or an uploaded image into cart-line candidates feeding cartBulkLookup. The cart_ai_provider_priority Setting controls provider order. |
The result feeds cartBulkLookup → bulkAddToCart so the AI parse, packaging match, and MMOQ preview share the same pipeline.
Payment Method Capabilities (Decided #233)
| Type | Description |
PaymentGatewayCapabilityStatus | Enum: SUPPORTED, PARTIAL, UNSUPPORTED, UNAVAILABLE |
PaymentMethodCapability | Per-gateway capability matrix |
PaymentMethodCapabilitySupports | Per-capability detail block (status + note) |
| Query | Signature |
paymentMethodCapabilities(channelId: ID!, includeOrphaned: Boolean = false, includeDisabled: Boolean = false): [PaymentMethodCapability!]! | Introspect what each configured gateway can do. Powers the admin gateway picker and feature gating in checkout. |
PaymentTender.source (DB column) carries provenance: cascade from approver, buyer_prepended from the buyer. The column is not currently exposed in GraphQL — readers go through the database for now.
Extension Federation (Decided #226, #227, #229)
| Type | Description |
AdminQuickActionType | Cmd-K entry contributed by an extension (href, label, iconName, permission?) |
AdminNavItemType | Sidebar entry contributed by an extension |
AdminPageTabType | Page-scoped tab contributed by an extension |
FederatedExtensionHit / FederatedExtensionSearchGroup | Extension search results merged into Cmd-K |
WorkflowFaultType | Row in the workflow-faults inbox |
WorkflowFaultFilters (input) | Filters for workflowFaults(filters) query |
| Operation | Signature |
workflowFaults(filters: WorkflowFaultFilters = null): [WorkflowFaultType!]! | Workflow fault inbox query |
uninstallExtension(extensionName: String!, force: Boolean! = false): Boolean! | Mark an extension uninstalled; InstallStateService persists the state (Decided #148). force=true bypasses pre_uninstall_check. |
updateSetting(...) | Generic per-channel settings override |
setMarketingPreference(accepts: Boolean!): CustomerType! | Storefront marketing-opt-in toggle (single boolean, no category granularity) |
myMarketingPreference: Boolean! | Top-level Query field for the signed-in customer's current preference |
Auto-Generated Reference
See Reference for the full auto-generated schema documentation including all types, inputs, queries, and mutations. Regenerate with:
cd vectis/backend
python ../../docs-site/scripts/gen_graphql_ref.py > ../../docs-site/docs/developer/graphql/reference.md