API reference
Entry point and index for ChannelWeave API routes and contracts.
Last updated:
Use this page as the API index, then open each focused page from the API sidebar menu.
Route groups
- Stock SKU policy
- Website webhooks
- Intelligence endpoints
- Health endpoints
- eBay listings import
- Media library routes
- Quick access pinned views
- Warehouse execution routes
Related references
Quick access pinned views API routes
GET /api/ui/pinned-views- session-authenticated (
requireAuth("json")) - tenant + user scoped (
customer_id,user_id) - returns the current user’s pinned quick-access views with canonical
hrefvalues
- session-authenticated (
POST /api/ui/pinned-views- session-authenticated (
requireAuth("json")) - tenant + user scoped (
customer_id,user_id) - JSON body:
path(required supported route path)search(optional query string for allowed filters)label(optional user label)
- supported paths:
/listings/listings/drafts/sales/orders/sales/orders/:id(sales order detail;:idmust be a UUID)/inbox/messages/:thread_id(conversation detail;:thread_idmust be a UUID)/listings/:channel/:listing_id(listing detail forebay|amazon|shopify|website)/inventory/stock/items/:stock_id(stock item detail;:stock_idmust be a UUID)/sales/buyers/:id(buyer detail;:idmust be a UUID)/inbox/messages/inbox/offers
- stores typed route data per route key (not arbitrary free-form URLs)
- session-authenticated (
DELETE /api/ui/pinned-views/:id- session-authenticated (
requireAuth("json")) - tenant + user scoped (
customer_id,user_id) - removes one pinned view by UUID id
- session-authenticated (
Media library API routes
GET /api/media/library- session-authenticated (
requireAuth("json")) - tenant-scoped by current session
customer_id - query:
page(default1)page_size(allowed25|50|100|250; default100)search(optional file-name contains filter)sort(optional; default-updated_at)- allowed:
file_name,-file_nameimage_url,-image_urlupdated_at,-updated_atsize_bytes,-size_bytes
- allowed:
- returns:
okbase_url(tenant media base URL)total(matching file count)pagepage_sizesortitems[]:file_nameimage_urlthumb_url(generated WebP thumbnail URL when available; falls back toimage_url)preview_url(generated WebP preview URL when available; falls back toimage_url)size_bytesupdated_atcontent_type
- session-authenticated (
POST /api/media/library/upload- session-authenticated (
requireAuth("json")) - tenant-scoped by current session
customer_id - multipart form-data body:
files(repeated) orfile(single)
- constraints:
- up to
25files per request - max
20 MBper file - allowed content types/extensions:
.jpg,.jpeg,.png,.webp,.gif,.avif,.svg
- up to
- response:
okbase_urlitems[](uploaded file metadata includingthumb_url/preview_url)
- variant generation:
- creates WebP
thumbandpreviewfiles for non-SVG uploads - keeps original aspect ratio (no forced square resize)
- size bounds:
thumb: max240x240preview: max960x960
- creates WebP
- session-authenticated (
POST /api/media/library/bulk-delete- session-authenticated (
requireAuth("json")) - tenant-scoped by current session
customer_id - JSON body:
file_names: string[](required)
- constraints:
- max
1000file names per request - file names are sanitised server-side before delete
- max
- response:
okrequested(unique validated file names from request)deletednot_found
- session-authenticated (
DELETE /api/media/library/:file_name- session-authenticated (
requireAuth("json")) - tenant-scoped by current session
customer_id - deletes one media file in the tenant products folder
- validates/sanitises file name server-side before delete
- session-authenticated (
Warehouse execution API routes
All Warehouse API routes are session-authenticated (requireAuth("json")) and
tenant-scoped by the current session customer_id.
POST /api/inventory/warehouse/scan/resolve- body:
scan_text(required)
- resolves one scan in this order:
- internal barcode
- manufacturer barcode
- SKU fallback
- bin code
- location code
- internal barcodes are unique per customer, so one internal-barcode scan resolves to one stock item only
- manufacturer barcodes can repeat across stock records; ambiguous matches are
rejected with
409until the stock identity is made explicit - response:
okmatch:- stock result with
balances[] - bin result with
contents[] - location result with totals
- stock result with
- body:
POST /api/inventory/warehouse/movements- body:
stock_idmovement_kind:putaway|move|replenishment|quarantinequantityfrom_location_id,from_bin_idto_location_id,to_bin_id- optional:
lot_code,expiry_date,reason_code,note,occurred_at
- response:
okmovement
- body:
POST /api/inventory/warehouse/adjustments- body:
stock_idlocation_id,bin_idquantity_deltareason_code- optional:
lot_code,expiry_date,note,occurred_at
- response:
okmovement
- body:
POST /api/inventory/warehouse/receiving- body:
stock_idreceived_quantitydestination_location_id,destination_bin_id- optional:
supplier_name,external_ref,condition_code,lot_code,expiry_date,note,occurred_at
- response:
okreceipt
- body:
POST /api/inventory/warehouse/returns- body:
stock_idreturned_quantitydisposition_code:restock|quarantine|damaged|inspectdestination_location_id,destination_bin_id- optional:
external_ref,condition_code,lot_code,expiry_date,note,occurred_at
- response:
okreceipt
- body:
POST /api/inventory/warehouse/counts- body:
stock_idcount_mode:cycle_count|spot_checklocation_id,bin_idcounted_quantity- optional:
lot_code,expiry_date,note,occurred_at
- response:
okcount
- body:
POST /api/inventory/warehouse/issues- body:
issue_typeseverity_codenote- optional:
stock_id,location_id,bin_id,photo_url
- response:
okissue
- body:
POST /api/inventory/warehouse/labels- body:
label_type:stock_barcode|bin_labelprint_quantity- for stock labels:
stock_id
- for bin labels:
bin_id
- optional:
note
- response:
okjobjob.print_hreffor the printable sheet
- notes:
- stock labels encode
stock.internal_barcodeas Code 128 - bin labels encode
stock_bin.bin_codeas Code 128 - manufacturer barcode is not used as the warehouse label identity
- label values must stay within printable ASCII so the sheet can render as Code 128
- stock labels encode
- body:
GET /api/inventory/warehouse/labels/:id- returns one label print job by UUID id
Listings bulk end route (Website)
POST /api/listings/website/end- session-authenticated (
requireAuth("json")) - tenant-scoped by current session
customer_id - used by Website listings list bulk action and Website listing detail End listing action
- body:
listing_ids: string[](required, Websitechannel_listing_idvalues)
- behaviour:
- ends Website listings in bulk by setting canonical
listing.statustoended - only Website channel rows are affected
- ends Website listings in bulk by setting canonical
- response:
okrequested(ids supplied)matched(Website rows found)ended(rows moved frompublishedtoended)already_endednot_found
- session-authenticated (
Listings archive routes (eBay + Website)
POST /api/listings/:channel/archive- session-authenticated (
requireAuth("json")) - tenant-scoped by current session
customer_id - supported channels:
ebaywebsite
- body:
listing_ids: string[](required)
- behaviour:
- archives canonical listing rows by setting
listing.status = 'archived' - eBay: rows in
published|endedcan be archived - Website: only rows in
endedcan be archived - Website rows still in
publishedare blocked and must be ended first
- archives canonical listing rows by setting
- response:
okchannelrequestedmatchedarchivedalready_archivedblocked_live(Website rows blocked because still live)not_found
- session-authenticated (
POST /api/listings/:channel/:listing_id/archive- single-listing variant of the same archive action
- same channel support and lifecycle rules as bulk archive
- for Website, returns
409when the listing is still live (published)
Overview dashboard API route
GET /api/dashboard- session-authenticated (
requireAuth("json")) - tenant-scoped by current session
customer_id - returns operational dashboard snapshot for
/dashboard - response shape:
ok,generated_atcards:- dispatch SLA leading indicators:
dispatch_sla_at_riskdispatch_sla_overduedispatch_sla_due_todaydispatch_sla_due_next_48hdispatch_sla_missing_ship_by
- late-shipment-rate (LSR) lagging indicators:
lsr_10d_rate_pct,lsr_10d_late_count,lsr_10d_eligible_countlsr_30d_rate_pct,lsr_30d_late_count,lsr_30d_eligible_count
unread_messages,unactioned_offersorders_in_progress,low_stock,sync_errors_24h
- dispatch SLA leading indicators:
links:- deep-links for each card area (
/sales/ordersdispatch-SLA filters, includinglate_10dandlate_30d, plus Inbox/low-stock/events)
- deep-links for each card area (
queues[]:to_allocate,to_pick,ready_to_dispatch,overdue_dispatch- each queue item includes
label,hint,count,href
channels[]:- orders-in-progress counts grouped by
channel_code/channel_label - each channel row includes
active_order_value_by_currency[]with per-channel in-progress value totals (after refund-ledger deductions) grouped bycurrency_code
- orders-in-progress counts grouped by
active_order_value_by_currency[]:- in-progress order value totals grouped by
currency_code - each row includes
currency_codeandactive_order_value(decimal text) - value formula per order/currency:
max(0, sales_order.total_gross_amount - sum(sales_order_refund.amount /* gross */)) - values are not FX-converted; when more than one currency is in play, UI should present this as mixed-currency totals
- in-progress order value totals grouped by
inventory_status:excess_inventory_skus,excess_inventory_valuestockout_risk_skus(projected stockout inside 14 days from 30-day velocity)never_sold_skusstagnant_90d_skus(on-hand stock with no sale in 90 days+)
reorder_horizon[]:- horizon summary rows for
7,14, and30days - each row includes
horizon_days,sku_count,projected_reorder_cost(decimal text)
- horizon summary rows for
cashflow_impact:excess_inventory_valueprojected_reorder_cost_14dstockout_revenue_risk_14d
urgent_replenishment[]:- top demand-risk SKU requiring reorder in the next 14 days
- includes
stock_id,sku,stock_name,quantity_available,quantity_on_order,avg_daily_qty,days_left,required_order_qty,required_order_value
fastest_moving[]:- SKU ranked by 30-day demand
- includes
stock_id,sku,stock_name,quantity_available,quantity_on_order,qty_30d,avg_daily_qty,days_left
stagnant_stock[]:- on-hand SKU ranked by oldest/no sale history
- includes
stock_id,sku,stock_name,quantity_on_hand,last_sale_at,days_since_sale,stock_value,never_sold
amazon_account_metrics:- account pulse snapshot for the connected Amazon channel
- fields:
connected(boolean)account_name(string|null)marketplace_name(string|null)published_listings_count(number|null)orders_30d_count(number|null)buyer_messages_30d_count(number|null)checked_at(ISO timestamp|null)unavailable_reason(string|null)
- source:
- current tenant
channel_connectionrow for account / marketplace identity when available - tenant-local
listingcount for published Amazon listings - tenant-local
sales_ordercount for last-30-day Amazon orders - tenant-local
message_thread/message_messagecount for last-30-day inbound Amazon buyer messages
- current tenant
- current UI use:
- Control tower Amazon account pulse card on
/dashboard
- Control tower Amazon account pulse card on
ebay_seller_metrics:- seller-level reputation snapshot for connected eBay accounts
- fields:
connected(boolean)seller_username(string|null)feedback_score(number|null)positive_feedback_percent(number|null)total_sold_count(number|null; current windowed value)sold_window_days(number|null; currently60when present)follower_count(number|null)captured_at(ISO timestamp|null)stale(boolean)followers_supported(boolean; currentlyfalsein v1)unavailable_reason(string|null)
- source:
- Trading API
GetUser+GetMyeBaySelling(SellingSummary + SoldList summary window)
- Trading API
- refresh behaviour:
- Control tower reads latest tenant snapshot from
channel_seller_metrics_snapshot - stale/missing snapshots trigger a refresh attempt and then persist the latest successful result
- Control tower reads latest tenant snapshot from
shopify_store_metrics:- store pulse snapshot for the connected Shopify store
- fields:
connected(boolean)store_name(string|null)shop_url(string|null; stored preferred host/domain)published_listings_count(number|null)orders_30d_count(number|null)buyer_messages_30d_count(number|null)checked_at(ISO timestamp|null)unavailable_reason(string|null)
- source:
- current tenant
channel_connectionrow for store identity - tenant-local
listingcount for published Shopify listings - tenant-local
sales_ordercount for last-30-day Shopify orders - tenant-local
message_thread/message_messagecount for last-30-day inbound Shopify buyer messages
- current tenant
- current UI use:
- Control tower Shopify store pulse card on
/dashboard
- Control tower Shopify store pulse card on
pos_store_metrics:- order pulse snapshot for the POS channel (manual/internal orders)
- fields:
enabled(boolean)orders_30d_count(number)open_orders_count(number)awaiting_payment_count(number)last_order_at(ISO timestamp|null)
- source:
- tenant-local
sales_orderrows withchannel_code = 'pos'
- tenant-local
- current UI use:
- Control tower POS order pulse card on
/dashboard
- Control tower POS order pulse card on
website_store_metrics:- store pulse snapshot for the Website channel connection
- fields:
connected(boolean)website_name(string|null)website_host(string|null; host/domain without protocol)published_listings_count(number|null)orders_30d_count(number|null)buyer_messages_30d_count(number|null)checked_at(ISO timestamp|null)unavailable_reason(string|null)
- source:
- current tenant
channel_connectionrow for Website identity/host - tenant-local
listingcount for published Website listings - tenant-local
sales_ordercount for last-30-day Website orders - tenant-local
message_thread/message_messagecount for last-30-day inbound Website buyer messages
- current tenant
- current UI use:
- Control tower Website store pulse card on
/dashboard
- Control tower Website store pulse card on
- session-authenticated (
Insights summary API route
GET /api/insights/summary- session-authenticated (
requireAuth("json")) - tenant-scoped by current session
customer_id - query:
since_hours(0= all time; default24when omitted/invalid)
- response shape:
since_hourscounts(info,warn,fail)items[](severity,title,message, optional action link)- current detector rows are always included so users can see explicit zero-count/healthy states in the selected view.
- current detectors:
- low stock (current-state stock position)
warnwhen one or more SKU are at/below reorder levelinfowhen no low stock risk exists
- recent error-like events in
event_log(windowed bysince_hours)warn/failwhen error-like events are presentinfowhen no error-like events are found
- manual actions on stock records (
event_type=manual_field_edit,entity_type=stock,source=user) inevent_log(windowed bysince_hours)- severity thresholds:
info:0-7editswarn:8-19editsfail:20+edits
- severity thresholds:
- refund-ledger signal from
sales_order_refund(windowed bysince_hours)failwhen refunded orders are not markedpayment_status in ('partially_refunded','refunded')warnwhen refund-entry volume is high in the selected windowinfowhen no refund mismatch/high-volume signal exists
- cancelled-order signal from
sales_order(windowed bysince_hoursagainstcoalesce(order_date, created_at))failwhen cancelled orders are not markedfulfilment_status = 'cancelled'warnwhen cancelled-order volume is high in the selected window (10+cancelled orders)infowhen cancellation signals are stable
- low stock (current-state stock position)
- session-authenticated (
Website connection details route
POST /api/channels/website/details- session-authenticated (
requireAuth("json")) - tenant-scoped by current session
customer_id - creates/updates the tenant Website
channel_connectionrow - body:
display_namewebsite_url(stored without protocol)orders_enabled(boolean, optional)stock_enabled(boolean, optional)
- response includes current persisted Website connection values for those fields.
- session-authenticated (
Website listings feed route
GET /api/website/listings- API-key authenticated (
Authorization: Bearer <website_api_key>) - tenant-scoped by the resolved Website channel connection
customer_id - query params:
page(default1)page_size(default50, max200)q(optional title/SKU/id search)sort(title,sku, or latest updated default)
- response fields:
ok,page,page_size,total,items[]
- row fields:
id(Websitechannel_listing_idwhen present; fallback internal UUID)sku,title,description,listing_type,currency,price,quantityview_urlimage_url(primary stock image URL for linked stock;nullwhen missing)updated_at,published_at
descriptionresolves as:listing.descriptionwhen non-empty- fallback
stock_product.descriptionfor linked stock rows
image_urlis resolved tenant-safely fromstock_product_imagethroughlisting.stock_id -> stock.stock_product_id.
- API-key authenticated (
Website webhook routes
POST /webhooks/website/orders/createdPOST /webhooks/website/orders/cancelled- public webhook routes (API key + signature verified)
- validates:
Authorization: Bearer <website_api_key>X-CW-TimestampX-CW-Signature(v1=<hmac_sha256_hex>)
- upserts tenant-scoped
sales_orderrows for channelwebsite orders/createdrequiresline_items[]and upsertssales_order_lineimmediately (SKU must exist in tenant stock)orders/creatednow also adjusts Website published-listing availability:- for each upserted order line, ChannelWeave computes the per-line quantity
delta vs the existing
sales_order_linerow - applies aggregated stock-linked deltas to
listing.quantityfor matching Website published listings (channel_code='website',status='published',listing.stock_id) - duplicate/retry webhooks with unchanged line quantities are idempotent
(
delta = 0, no further listing decrement) - listing status is not auto-ended by this webhook; quantity can reach
0while listing remains published until ended explicitly
- for each upserted order line, ChannelWeave computes the per-line quantity
delta vs the existing
orders/creatednow also resolves Website buyers to Sales buyer master records when a buyer email is present:- identity key: (
channel_code='website',identity_type='email',identity_value=lower(buyer_email)) - reuses existing mapped buyer when found; otherwise auto-creates buyer +
identity link and sets
sales_order.buyer_id - order snapshot fields (
buyer_name,buyer_email,buyer_phone) are still stored onsales_order
- identity key: (
orders/cancelledmarks order + lines as cancelled
POST /webhooks/website/messages/contact- public signed webhook route for website contact submissions
- validates:
Authorization: Bearer <website_api_key>X-CW-TimestampX-CW-Signature(v1=<hmac_sha256_hex>)
- expects JSON payload with
name,email,message, and idempotency key (submission_idorX-CW-Idempotency-Key) - creates/updates an inbound website buyer-question conversation in Inbox → Messages
Shopify webhook routes
POST /webhooks/shopify/orders/paid- public webhook route (Shopify HMAC-verified)
- validates
X-Shopify-Hmac-Sha256using the configured Shopify app secret - resolves tenant by
X-Shopify-Shop-Domain - upserts tenant-scoped Shopify
sales_orderpayment state and writeschannel_payment_eventledger rows
POST /webhooks/shopify/customers/createPOST /webhooks/shopify/customers/update- public webhook routes (Shopify HMAC-verified)
- validates
X-Shopify-Hmac-Sha256using the configured Shopify app secret - resolves tenant by
X-Shopify-Shop-Domain - ingests inbound Inbox messages only when payload
noteis non-empty - stores inbound messages under
channel_code='shopify'
POST /webhooks/shopify/proxy/contact- public Shopify app-proxy route for storefront custom contact forms
- validates signed app-proxy query parameters (
shop,timestamp,signature) using the configured Shopify app secret - rejects stale signed requests (>15 minutes from current server time)
- accepts URL-encoded form payloads and JSON payloads with message content
(
contact[body]/message) - stores inbound messages under
channel_code='shopify'with message typebuyer_question
Sales pick-list PDF route
GET /sales/orders/pick-list/pdf- session-authenticated (
requireAuth("html")) - tenant-scoped by
customer_id - query:
order_ids=<uuid[,uuid...]> - returns
application/pdf - supports one or many orders (current max: 200 per request)
- PDF sections:
- consolidated pick lines grouped by location/bin/SKU
- per-order outstanding line breakdown
- session-authenticated (
Sales order partial-refund route
POST /sales/orders/:id/refund-partial- session-authenticated (
requireAuth("html")) - tenant-scoped by current
customer_id - eBay orders only (
channel_code = 'ebay') - submitted form contract:
refund_amount(gross, required, positive, max 2dp, partial-only)refund_tax_amount(VAT split amount)- required when order VAT total is greater than zero
- must be
0.00for zero-rated orders
refund_reason_code(required; eBay reason enum)refund_comments(optional, max 100 chars)
- writes tenant-scoped
sales_order_refundledger row withamount/net_amount/tax_amountsplit andtax_breakdown_status - refund status in that row reflects immediate
issue_refundresponse (oftenPENDING) and is later reconciled by eBay order sync (paymentSummary.refunds[]status) - outbound eBay API request still sends gross only in
orderLevelRefundAmount
- session-authenticated (
Sales order edit-save route
POST /sales/orders/:id- session-authenticated (
requireAuth("html")) - tenant-scoped by current
customer_id - status guard contract:
- manual Save accepts only
open|pending|processing|confirmed dispatchedandcancelledare action-driven only (ship/cancel actions)- once order status is
dispatchedorcancelled, edit-save cannot change it
- manual Save accepts only
- payment-status contract:
payment_statusis read-only on the form and is not updated by this route- payment status changes are system-driven (channel payment/refund flows)
- currency-code contract:
currency_codeis editable only on new-order create and is read-only on existing order detail- this route does not update
currency_code - crafted attempts to change currency are rejected
- fulfilment-status contract:
fulfilment_statusis read-only on the form and is not updated by this route- fulfilment status changes are system-driven (allocation/ship/cancel and channel sync flows)
- canonical values are:
open|partial|allocated|dispatched|cancelled
- session-authenticated (
Sales order POS payment-capture route
POST /sales/orders/:id/payment/pos/capture- session-authenticated (
requireAuth("html")) - tenant-scoped by current
customer_id - POS orders only (
channel_code = 'pos') - submitted form contract:
payment_capture_amount(required, positive, max 2dp)payment_method(cash|card|bank_transfer|other)payment_external_ref(optional)payment_event_at(optional local date/time input)
- writes tenant-scoped
sales_order_payment_eventledger row withevent_type='capture' - enforces cumulative guardrail:
sum(capture amounts) + new amount <= sales_order.total_gross_amount - updates
sales_order.payment_statusto:paidwhen captured total reaches gross totalpartially_paidotherwise
- session-authenticated (
Sales buyers HTML routes
GET /sales/buyers- session-authenticated (
requireAuth("html")) - tenant-scoped by
customer_id - query:
q(search by buyer code/name/group/contact fields)status=active|inactive(optional)sort=buyer_name_asc|buyer_name_desc|buyer_code_asc|buyer_code_desc|updated_desc|updated_asc(optional)page,page_size
- session-authenticated (
GET /sales/buyers/new- session-authenticated (
requireAuth("html")) - tenant-scoped by
customer_id - renders buyer create form
- session-authenticated (
POST /sales/buyers/new- session-authenticated (
requireAuth("html")) - tenant-scoped by
customer_id - creates one
buyerrow (requiresbuyer_code,buyer_name)
- session-authenticated (
GET /sales/buyers/:id- session-authenticated (
requireAuth("html")) - tenant-scoped by
customer_id - renders buyer detail/edit form
- session-authenticated (
POST /sales/buyers/:id- session-authenticated (
requireAuth("html")) - tenant-scoped by
customer_id - updates one
buyerrow
- session-authenticated (
Sales buyer-groups HTML routes
GET /sales/buyer-groups- session-authenticated (
requireAuth("html")) - tenant-scoped by
customer_id - query:
q(search by group code/name/parent fields)sort=group_name_asc|group_name_desc|group_code_asc|group_code_desc|buyer_count_desc|buyer_count_asc|updated_desc|updated_asc(optional)page,page_size
- session-authenticated (
GET /sales/buyer-groups/new- session-authenticated (
requireAuth("html")) - tenant-scoped by
customer_id - renders buyer-group create form
- session-authenticated (
POST /sales/buyer-groups/new- session-authenticated (
requireAuth("html")) - tenant-scoped by
customer_id - creates one
buyer_grouprow (requiresgroup_code,group_name)
- session-authenticated (
GET /sales/buyer-groups/:id- session-authenticated (
requireAuth("html")) - tenant-scoped by
customer_id - renders buyer-group detail/edit form
- session-authenticated (
POST /sales/buyer-groups/:id- session-authenticated (
requireAuth("html")) - tenant-scoped by
customer_id - updates one
buyer_grouprow
- session-authenticated (
POST /sales/buyer-groups/:id/delete- session-authenticated (
requireAuth("html")) - tenant-scoped by
customer_id - deletes one
buyer_grouprow
- session-authenticated (
eBay orders sync route
POST /api/ebay/orders/sync- session-authenticated (
requireAuth("json")) - tenant-scoped by current session
customer_id - optional query:
lookback_hours(default24, min1, max720)max_pages(default10, min1, max100)
- returns JSON:
ok: truesummarywith customer-level ingest counts (pages_fetched,fetched_orders,processed_orders,inserted_orders,updated_orders,inserted_lines,updated_lines,skipped_orders,failed_orders,errors)
- session-authenticated (
Theme preference endpoint
POST /settings/theme- session-authenticated (
requireAuth("json")) - tenant-scoped and user-scoped by
(customer_id, user_base.id)from the active session - request body (
application/x-www-form-urlencoded):theme=default|neon-noir|rose-noir|aurora|linen|eden|emerald-noir|lavender-noir
- writes
user_base.ui_themeand updates the in-session user object - response JSON:
- success:
{ "ok": true, "theme": "<value>" } - validation failure:
400 - unauthorised:
401
- success:
- session-authenticated (
Listings stock-link endpoints
ChannelWeave now uses draft-first stock-link editing in UI:
Create/link stock from draft editors:
POST /api/listings/website/drafts/:id/create-and-link-stock- body:
{ "sku": "<text>", "stock_group_id": "<uuid|null>" }- optional deferred group create:
{ "stock_group_create": { "group_code": "<text>", "group_name": "<text>" } }
- body:
POST /api/listings/ebay/drafts/:id/create-and-link-stock- body:
{ "sku": "<text>" }
- body:
POST /api/listings/ebay/drafts/:id/link-stock- body:
{ "stock_id": "<uuid>" } - supports replacing an existing stock link.
- replacement is blocked (
409) when eBay policy-risk validation fails (for example active best-offer activity) or when variation rows exist.
- body:
POST /api/listings/ebay/drafts/:id/link-stock/:stock_id- body-free variant of draft link/replace.
- same policy validation and
409behaviour as.../link-stock.
- both routes are session-authenticated, tenant-scoped by
customer_id. - Website draft stock-link routes (
create-and-link-stock,link-stock, and draft save withstock_id) now includestock_image_prefill_countin responses when stock images are auto-seeded into an empty Website draft from linked stock product images. - Website draft stock-link creation flows now also default empty draft fields
from linked stock:
description←stock_product.descriptionwhen draft description is emptycategory_path←stock_group.group_namewhen draft category path is emptycategory_id←stock_group.group_codewhen draft category ID is emptyquantity:stock.quantity_availablewhen stock quantity signal exists- fallback
1only when no stock quantity signal exists
price:- enforced policy price for non-manual pricing modes
- manual mode fallback to
stock.sale_pricewhen draft price is empty
:listing_idnow resolves fromlisting.channel_listing_id:- eBay: eBay Item ID (for example
327001255236) - Website: ChannelWeave website listing ID (for example
WL-000003)
- eBay: eBay Item ID (for example
Published listing link routes remain available for create/link operations:
POST /api/listings/:channel/:listing_id/create-and-link-stock- body:
{ "sku": "<text>", "stock_group_id": "<uuid|null>" }- optional deferred group create:
{ "stock_group_create": { "group_code": "<text>", "group_name": "<text>" } }
- channels:
ebay,website - applies SKU policy before create/link.
- rejects
stock_group_idwhen the target group is disabled. - rejects requests that provide both
stock_group_idandstock_group_create. - when
stock_group_createis supplied, group creation is deferred and performed only inside the create-and-link transaction.
- body:
POST /api/listings/:channel/:listing_id/link-stock- body:
{ "stock_id": "<uuid>" } - channels:
ebay,website
- body:
POST /api/listings/:channel/:listing_id/unlink-stock- body:
{} - channels:
ebay,website - returns
409(direct unlink blocked; use Revise via draft then Change stock item).
- body:
POST /api/listings/website/drafts/:id/unlink-stock- body:
{} - returns
409(direct unlink blocked; use Change stock item).
- body:
POST /api/listings/:channel/archive- body:
{ "listing_ids": ["<listing_id>", ...] } - channels:
ebay,website - eBay archives from
published|ended. - Website archives only from
ended(live Website listings must be ended first).
- body:
POST /api/listings/:channel/:listing_id/archive- body:
{} - channels:
ebay,website - single-listing archive variant.
- body:
All routes above are session-authenticated and tenant-scoped by customer_id.
- draft routes operate on
listing_draftrows. - Website draft image route:
GET /api/listings/website/drafts/:id/images- when the draft image list is empty and the draft is linked to stock,
images are auto-seeded from
stock_product_imagebefore returning items.
- when the draft image list is empty and the draft is linked to stock,
images are auto-seeded from
POST /api/listings/website/drafts/:id/images- body:
{ "image_url": "<absolute-url>" } - accepts tenant ChannelWeave Media URLs only (
/products/<tenant>/...). - appends image to the end of the draft image order.
- body:
POST /api/listings/website/drafts/:id/images/reorder- body:
{ "image_ids": ["<uuid>", ...] } - requires every current draft image id exactly once.
- updates
listing_draft_image.sort_orderto match provided order.
- body:
POST /api/listings/website/drafts/:id/images/sync-from-stock-manifest- body:
{} - requires linked stock on the draft.
- syncs missing tenant ChannelWeave Media images from linked stock product image manifest into the draft image manifest.
- returns
added_count.
- body:
DELETE /api/listings/website/drafts/:id/images/:image_id- deletes one draft image row.
- draft delete route:
DELETE /api/listings/website/drafts/:id- deletes one Website
listing_draftrow for the current tenant. - before delete, clears
listing.draft_idon tenant Website listings linked to that draft so published listings remain valid. - returns
404when the draft is not found.
- deletes one Website
- draft archive routes:
POST /api/listings/website/drafts/:id/archive- sets
listing_draft.status = 'archived' - returns
409when the draft is currentlypublishing
- sets
POST /api/listings/website/drafts/:id/unarchive- sets
listing_draft.status = 'draft'from archived state - returns
409when draft is not archived or currently publishing
- sets
POST /api/listings/ebay/drafts/:id/archive- sets
listing_draft.status = 'archived' - returns
409when the draft is currentlypublishing
- sets
POST /api/listings/ebay/drafts/:id/unarchive- sets
listing_draft.status = 'draft'from archived state - returns
409when draft is not archived or currently publishing
- sets
- published-listing routes operate on canonical
listingrows (status in ('published', 'ended', 'archived')). - Website draft publish route lifecycle:
POST /api/listings/website/drafts/:id/publish- sets
listing_draft.status = 'publishing'while publish is in flight - sets
listing_draft.status = 'published'on success - sets
listing_draft.status = 'error'andlast_erroron failure - returns
409when the draft is already publishing
- sets
List endpoints (thumbnail metadata)
The following list endpoints now include image-preview metadata in each row:
GET /api/inventory/stock/itemsGET /api/inventory/stock/lowGET /api/listings/draftsGET /api/listings
Row-level image fields:
image_url(string | null)- canonical source image URL when a product/listing image exists
thumb_url(string | null)- thumbnail variant URL for list rendering
nullwhen a thumbnail variant is unavailable (for example external-hosted image URLs)
has_image(boolean)truewhen an image association exists even ifthumb_urlisnull
Published listings rows (GET /api/listings) also include:
description(string | null)- listing description text used for the Item-column preview summary
Draft listings rows (GET /api/listings/drafts) also include:
description(string | null)- draft description text used for the Item-column preview summary
Listing revision-draft endpoints
POST /api/listings/ebay/revision-drafts- body:
{ "listing_id": "<ebay_item_id>" } - creates a new eBay revision draft for a published listing.
- body:
GET /api/listings/ebay/:listing_id/revision-diff- query:
left_draft_id=<uuid>right_draft_id=<uuid>
- compares two eBay drafts from the same listing history and returns field-level changes.
- query:
GET /api/listings/:channel/:listing_id- for eBay listing detail responses, payload now includes
listing.revision_history[]:draft_idversion,version_label(Original,Revision n)statusis_live_sourcecompare_from_draft_idcreated_at,updated_at, and SKU/title summary fields
- for eBay listing detail responses, payload now includes
POST /api/listings/website/revision-drafts- body:
{ "listing_id": "<website_channel_listing_id>" }(for exampleWL-000003) - for a published Website listing:
- reuses existing linked draft when
listing.draft_idstill exists - otherwise creates a new draft from published listing fields +
listing.publish_snapshot, linkslisting.draft_id, and returns editor URL
- reuses existing linked draft when
- response includes
draft_id,listing_id,edit_url, and optionalreused: true.
- body:
Inventory barcode policy routes
GET /api/inventory/stock/barcode-policy- returns configured customer barcode policy if present
- otherwise returns the effective default policy
- response includes:
configuredpolicyeffective
POST /api/inventory/stock/barcode-policy- body:
internal_prefixinternal_sequence_widthinternal_symbology:code_128manufacturer_barcode_requirement:optional|recommended|required
- rule:
- ChannelWeave always generates an internal barcode on stock create when the request does not provide one
- body:
Stock create/update payloads now accept these barcode fields:
internal_barcodemanufacturer_barcodemanufacturer_barcode_format:upc_a|ean_13|ean_8|isbn_13|gtin_14|unknown|other
Stock group detail endpoint
GET /api/inventory/stock/groups- optional query:
active=true|false - each returned group now includes:
is_activestockcountlow_stock_count
- UI can derive group status:
- disabled when
is_active = false - low stock when
low_stock_count > 0 - healthy when
stockcount > 0andlow_stock_count = 0 - no stock when
stockcount = 0
- disabled when
- optional query:
GET /api/inventory/stock/groups/lookup- optional query:
active=true|false - returns
id,group_code,group_name,is_active
- optional query:
POST /api/inventory/stock/groups- accepts optional
is_activeboolean (defaults totrue)
- accepts optional
POST /api/inventory/stock/groups/:id- accepts optional
is_activeboolean update
- accepts optional
DELETE /api/inventory/stock/groups/:id- deletes one stock group by UUID for the current tenant
- linked stock rows are preserved and become ungrouped (
stock_group_idbecomesnull) - response includes deleted group summary and linked stock count at delete time
Stock item delete endpoint
DELETE /api/inventory/stock/items/:id- session-authenticated (
requireAuth("json")), tenant-scoped bycustomer_id - deletes one stock item by UUID from the current tenant
- removes any
stock_bin_assignmentrows linked to that stock item - listing/listing-draft links are cleared by FK
ON DELETE SET NULL - returns
409when operational/history links exist (for example sales order lines, purchase order lines, credit note lines, stock adjustments, stock-check rows, or stock transfers) - when the deleted stock item was the last row referencing its
stock_product_id, the orphanedstock_productrow is deleted and related product-level rows are removed via cascade
- session-authenticated (
Stock item image endpoints
GET /api/inventory/stock/items/:id/images- session-authenticated (
requireAuth("json")), tenant-scoped bycustomer_id - returns product-level image rows from
stock_product_image - when no rows exist, the route attempts a media-library SKU sync first, then returns refreshed rows
- session-authenticated (
POST /api/inventory/stock/items/:id/images- session-authenticated (
requireAuth("json")), tenant-scoped bycustomer_id - body:
image_url: string(required, absolutehttp(s))
- accepts tenant ChannelWeave Media URLs only (
/products/<tenant>/...) - upserts one
stock_product_imagerow (appended to the current manifest order when new)
- session-authenticated (
POST /api/inventory/stock/items/:id/images/sync-from-media- session-authenticated (
requireAuth("json")), tenant-scoped bycustomer_id - syncs media-library files into
stock_product_imageby SKU filename match:- exact:
<sku>.<ext> - suffixed:
<sku>-<suffix>.<ext>
- exact:
- response includes:
matched_countadded_countmatched_file_names[]matched_image_urls[]
- returns
409when stock SKU is missing
- session-authenticated (
POST /api/inventory/stock/items/:id/images/reorder- session-authenticated (
requireAuth("json")), tenant-scoped bycustomer_id - body:
image_ids: string[](required UUID list)- must include every current stock-product image exactly once
- rewrites
stock_product_image.positionto match provided order
- session-authenticated (
Stock adjustment endpoints
GET /api/inventory/stock/adjustments- session-authenticated and tenant-scoped by
customer_id - supports optional query:
search(SKU, stock name, note, location code)type=manual_adjustment|stock_check|transferstock_id=<uuid>location_id=<uuid>(matches adjustment location, source, or target)date_from=YYYY-MM-DDdate_to=YYYY-MM-DDpage,page_size
- returns inventory-wide adjustment rows with
stock_skuandstock_namefields for list rendering
- session-authenticated and tenant-scoped by
GET /api/inventory/stock/items/:id/adjustments- session-authenticated and tenant-scoped by
customer_id - supports optional query:
type=manual_adjustment|stock_check|transferpage,page_size
- returns adjustment rows with location context and linked stock-check / transfer metadata where present
- session-authenticated and tenant-scoped by
POST /api/inventory/stock/items/:id/adjustments- session-authenticated and tenant-scoped by
customer_id - body requires
adjustment_type:manual_adjustment:quantity_delta(non-zero numeric)- optional
location_id,note,occurred_at - updates
stock.quantity_on_handby delta
stock_check:counted_quantity(numeric,>= 0)- optional
location_id,note,occurred_at - writes
stock_check_resultand setsstock.quantity_on_handto counted quantity
transfer:source_location_id,target_location_id(different UUIDs)transfer_quantity(numeric,> 0)- optional
note,occurred_at - writes
stock_transferand transfer adjustment audit row without changing globalstock.quantity_on_hand
- session-authenticated and tenant-scoped by
All adjustment writes are transactional (BEGIN/COMMIT with rollback on
validation or write failure).
Inbox messages threads endpoint
GET /api/messages/threads- supports optional
sync=1query to force immediate eBay read-state sync before the list query. - supports optional
state=active|archived|all|deletedmaintenance filter (defaultactive). - supports optional
message_typefilter:buyer_questionoffer_updateorder_financialorder_cancellationreturns_casesshipping_fulfilment_updateaccount_noticeother_uncategorised
- each thread row includes:
message_type_codeandmessage_type_labellast_message_direction(inbound|outbound|null)message_preview(single-line, trimmed preview, nullable)archived_at,deleted_at, and derivedthread_state
- supports optional
POST /api/messages/threads/:id/archivePOST /api/messages/threads/:id/unarchivePOST /api/messages/threads/:id/deletePOST /api/messages/threads/:id/restore- maintenance actions for Inbox thread lifecycle management
- all actions are tenant-scoped and return updated
thread_state