labx-api
LabX Seller API Integration Skill
Overview
LabX Seller API provides programmatic access for managing product listings, inquiries, and orders on the LabX marketplace. This skill documents the complete API surface, integration patterns, and ALT-specific implementation details.
Base URL: https://admin.labx.com
Development URL: http://localhost (not typically used)
API Version: v1
Path Prefix: /api/v1/ (use this prefix for all endpoints)
All authenticated endpoints require a Bearer token via the seller-api-token header. The token is stored in ENV['LABX_API_TOKEN'] and is active for the duration of ALT's listing program (not time-limited).
Authentication
Bearer Token Authentication
All authenticated endpoints require the seller-api-token header:
Authorization: Bearer <seller-api-token>
Token Management:
- 1Password: Stored in
Developmentvault as "LabX Seller API Token" (credential field) - Retrieval:
op item get "LabX Seller API Token" --vault="Development" --fields credential - ENV variable: The application reads from
ENV['LABX_API_TOKEN']at runtime - Token lifetime: Active for length of ALT listing program (no expiration)
- Token persistence: Same token survives program renewals
- Token rotation: Contact Jesse Brito at LabX to rotate if compromised
- Token validation: If no active program, token will not work
Retrieving the token when needed:
# Get the token value from 1Password
op item get "LabX Seller API Token" --vault="Development" --fields credential
# Set it as an environment variable for local development
export LABX_API_TOKEN=$(op item get "LabX Seller API Token" --vault="Development" --fields credential)
# Use in a one-off command
LABX_API_TOKEN=$(op read "op://Development/LabX Seller API Token/credential") rails console
No OAuth Flow Required: Token is provisioned by LabX directly. No client credentials or OAuth dance needed.
API Endpoints Reference
Lookup Endpoints (No Authentication Required)
These endpoints provide reference data for categories, countries, and manufacturers. No seller-api-token required per Swagger documentation.
GET /api/v1/categories
Retrieve all available product categories.
HTTP Method: GET
Path: /api/v1/categories
Authentication: None required
Query Parameters: None
Response 200:
{
"success": true,
"data": {
"categories": [
{
"id": 0,
"name": "string"
}
]
}
}
Example cURL:
curl -X GET "https://admin.labx.com/api/v1/categories" \
-H "accept: application/json"
GET /api/v1/countries
Retrieve all available countries with regions.
HTTP Method: GET
Path: /api/v1/countries
Authentication: None required
Query Parameters: None
Response 200:
{
"success": true,
"data": {
"countries": [
{
"code": "US",
"name": "string",
"regions": [
{
"id": 0,
"name": "string"
}
]
}
]
}
}
Example cURL:
curl -X GET "https://admin.labx.com/api/v1/countries" \
-H "accept: application/json"
GET /api/v1/manufacturers
Retrieve all available manufacturers.
HTTP Method: GET
Path: /api/v1/manufacturers
Authentication: None required
Query Parameters: None
Response 200:
{
"success": true,
"data": {
"manufacturers": [
{
"id": 0,
"name": "string"
}
]
}
}
Example cURL:
curl -X GET "https://admin.labx.com/api/v1/manufacturers" \
-H "accept: application/json"
Product Endpoints (Authentication Required)
GET /api/v1/listings
Retrieve paginated list of product listings with filtering options.
HTTP Method: GET
Path: /api/v1/listings
Authentication: Required (seller-api-token header)
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
| page | integer | 1 | Page number for pagination |
| page_length | integer | 1000 | Number of items per page (default 1000, max undefined but >1000 may timeout) |
| show_active | boolean | - | Filter for active listings |
| show_inactive | boolean | - | Filter for inactive listings |
| search | string | - | Search term for filtering listings |
Response 200:
{
"success": true,
"data": {
"meta": {
"page": 1,
"page_length": 1000,
"total_pages": 5,
"total_records": 4532,
"filters": {
"show_active": true,
"show_inactive": false,
"search": "string"
}
},
"items": [
{
"unique_id": 0,
"sku": "string",
"name": "string",
"manufacturer": "string",
"model": "string",
"description": "string",
"listing_active": 0,
"condition": "Refurbished",
"warranty_offered": 1,
"price": 0,
"currency": "USD",
"qty": 0,
"min_order_qty": 0,
"weight": 0,
"weight_units": "KG",
"height": 0,
"width": 0,
"length": 0,
"size_units": "INCHES",
"show_quote_button": 1,
"show_buy_now_button": 1,
"external_link_url": "string",
"external_link_text": "string",
"categories": [],
"location": {},
"shipping": [],
"images": [],
"options": {},
"variations": {}
}
]
}
}
Field Type Notes:
unique_id: Returned as integer (not string)listing_active: Returned as integer (0 or 1, not boolean)condition: Returned in Titlecase (e.g., "Refurbished", "New", "Used", "Parts")
Example cURL:
curl -X GET "https://admin.labx.com/api/v1/listings?page=1&page_length=1000&show_active=true" \
-H "accept: application/json" \
-H "seller-api-token: Bearer ${LABX_API_TOKEN}"
POST /api/v1/listings/upload
Batch create or update product listings. Matching by unique_id: if it exists, the listing is updated; if new, a listing is created.
HTTP Method: POST
Path: /api/v1/listings/upload
Authentication: Required (seller-api-token header)
Content-Type: application/json
Request Body:
{
"items": [
{
"unique_id": "string",
"name": "string",
"manufacturer": "string",
"model": "string",
"description": "string",
"condition": "REFURBISHED",
"warranty_offered": true,
"price": 0,
"currency": "USD",
"qty": 0,
"min_order_qty": 0,
"weight": 0,
"weight_units": "KG",
"height": 0,
"width": 0,
"length": 0,
"size_units": "INCHES",
"show_quote_button": true,
"show_buy_now_button": true,
"external_link_url": "string",
"external_link_text": "string",
"categories": [],
"location": {},
"shipping": [],
"images": []
}
]
}
Required Fields:
unique_id(string, 1-100 characters)name(string, 3-500 characters)
Field Enums and Constraints:
| Field | Type | Values | Notes |
|---|---|---|---|
| condition | string | NEW, USED, PARTS, REFURBISHED | UPPERCASE, case-sensitive |
| currency | string | USD, CAD, GBP, EUR | |
| price | decimal | 0 - 99,999,999.99 | |
| weight_units | string | LBS, KG | |
| size_units | string | INCHES, CM | |
| shipping.carrier | string | FEDEX, UPS, USPS, FLAT, PICKUP | |
| shipping.type | string | CALCULATED, FLAT |
Important POST vs GET Differences:
- POST unique_id: string (GET returns integer)
- POST listing_active: boolean (GET returns 0/1 integer)
- POST condition: UPPERCASE (GET returns Titlecase)
Response 200:
{
"success": true,
"data": {
"testMode": false,
"validListingCount": 145,
"invalidListingCount": 2,
"invalidListingDetails": [
{
"unique_id": "string",
"errors": ["error message"]
}
]
}
}
Example cURL:
curl -X POST "https://admin.labx.com/api/v1/listings/upload" \
-H "accept: application/json" \
-H "seller-api-token: Bearer ${LABX_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"unique_id": "ALT-12345",
"name": "Thermo Fisher Centrifuge",
"condition": "REFURBISHED",
"price": 2500.00,
"currency": "USD"
}
]
}'
ALT Implementation Note: The actual ALT codebase wraps the array in a "listings" key instead of "items", which may be legacy behavior. Swagger documentation uses "items".
Inquiry Endpoints (Authentication Required)
GET /api/v1/inquiries
Retrieve list of inquiries. Use after_id parameter for incremental fetching.
HTTP Method: GET
Path: /api/v1/inquiries
Authentication: Required (seller-api-token header)
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| after_id | integer | Highest known messageId. Returns only inquiries received after this ID. |
Response 200:
{
"success": true,
"data": [
{
"inquiryId": 0,
"messageId": 0,
"type": "sent",
"messageAt": "string"
}
]
}
Example cURL:
curl -X GET "https://admin.labx.com/api/v1/inquiries?after_id=1000" \
-H "accept: application/json" \
-H "seller-api-token: Bearer ${LABX_API_TOKEN}"
GET /api/v1/inquiries/{inquiryId}
Retrieve full details for a specific inquiry including customer info and listing details.
HTTP Method: GET
Path: /api/v1/inquiries/{inquiryId}
Authentication: Required (seller-api-token header)
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| inquiryId | integer | Yes | Inquiry ID to retrieve |
Response 200:
{
"success": true,
"data": {
"quoteRequestId": 0,
"sellerId": 0,
"sellerName": "string",
"customer": {
"email": "string",
"phone": "string",
"name": "string",
"company": "string",
"street": "string",
"city": "string",
"region": "string",
"countryCode": "US",
"postalCode": "string"
},
"listing": {
"id": 0,
"sellerItemNumber": "string",
"sku": "string",
"name": "string",
"manufacturer": "string",
"model": "string",
"price": 0,
"variation": [{}],
"link": "string",
"image": "string"
},
"category": "string",
"quotePreferences": {
"timeline": "Info only",
"equipmentType": "Complete System",
"includeNew": true,
"includeUsed": true
}
},
"context": {
"quoteId": 0
}
}
Example cURL:
curl -X GET "https://admin.labx.com/api/v1/inquiries/12345" \
-H "accept: application/json" \
-H "seller-api-token: Bearer ${LABX_API_TOKEN}"
Order Endpoints - Sales (Authentication Required)
GET /api/v1/orders/sales
Retrieve paginated list of sales orders (items sold by ALT).
HTTP Method: GET
Path: /api/v1/orders/sales
Authentication: Required (seller-api-token header)
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| page | integer | Page number for pagination |
| orders_since | string | ISO 8601 datetime. Return orders created after this timestamp. |
| orders_after_id | integer | Return orders with ID greater than this value |
Response 200:
{
"success": true,
"data": [
{
"order_id": 0,
"created_at": "string",
"customer_email": "string",
"num_skus": 0,
"num_items": 0,
"subtotal": 0,
"tax": 0,
"total": 0,
"skus": "string"
}
],
"context": {
"page": 1,
"pageSize": 1,
"totalPages": 0,
"totalOrders": 0,
"filters": {
"orders_since": "2024-01-01T00:00:00Z",
"orders_after_id": 0
}
}
}
Example cURL:
curl -X GET "https://admin.labx.com/api/v1/orders/sales?page=1&orders_since=2024-01-01T00:00:00Z" \
-H "accept: application/json" \
-H "seller-api-token: Bearer ${LABX_API_TOKEN}"
GET /api/v1/orders/sales/{orderId}
Retrieve full details for a specific sales order.
HTTP Method: GET
Path: /api/v1/orders/sales/{orderId}
Authentication: Required (seller-api-token header)
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| orderId | integer | Yes | Sales order ID to retrieve |
Response 200:
{
"success": true,
"data": {
"orderId": 0,
"orderedAt": "string",
"total": 0,
"paymentMethod": "string",
"customer": {
"email": "string",
"phone": "string",
"name": "string",
"company": "string",
"street": "string",
"city": "string",
"postalcode": "string",
"region": "string",
"country": "US"
},
"items": [
{
"sku": "string",
"name": "string",
"variation": {},
"price": 0,
"qty": 0,
"subtotal": 0,
"shipMethod": "string",
"shipPrice": 0,
"tax": 0,
"total": 0
}
]
}
}
Example cURL:
curl -X GET "https://admin.labx.com/api/v1/orders/sales/12345" \
-H "accept: application/json" \
-H "seller-api-token: Bearer ${LABX_API_TOKEN}"
Order Endpoints - Purchases (Authentication Required)
GET /api/v1/orders/purchases
Retrieve paginated list of purchase orders (items purchased by ALT from other sellers).
HTTP Method: GET
Path: /api/v1/orders/purchases
Authentication: Required (seller-api-token header)
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| page | integer | Page number for pagination |
| orders_since | string | ISO 8601 datetime. Return orders created after this timestamp. |
| orders_after_id | integer | Return orders with ID greater than this value |
Response 200:
{
"success": true,
"data": [
{
"order_id": 0,
"created_at": "string",
"num_sellers": 0,
"num_skus": 0,
"num_items": 0,
"subtotal": 0,
"tax": 0,
"total": 0,
"skus": "string",
"sellers": "string"
}
],
"context": {
"page": 1,
"pageSize": 1,
"totalPages": 0,
"totalOrders": 0,
"filters": {
"orders_since": "2024-01-01T00:00:00Z",
"orders_after_id": 0
}
}
}
Example cURL:
curl -X GET "https://admin.labx.com/api/v1/orders/purchases?page=1&orders_since=2024-01-01T00:00:00Z" \
-H "accept: application/json" \
-H "seller-api-token: Bearer ${LABX_API_TOKEN}"
GET /api/v1/orders/purchases/{orderId}
Retrieve full details for a specific purchase order.
HTTP Method: GET
Path: /api/v1/orders/purchases/{orderId}
Authentication: Required (seller-api-token header)
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| orderId | integer | Yes | Purchase order ID to retrieve |
Response 200:
{
"success": true,
"data": {
"orderId": 0,
"orderedAt": "string",
"total": 0,
"paymentMethod": "string",
"sellers": [
{
"seller": "string",
"email": "string",
"phone": "string",
"location": "string",
"items": [
{
"sku": "string",
"name": "string",
"variation": {},
"price": 0,
"qty": 0,
"subtotal": 0,
"shipMethod": "string",
"shipPrice": 0,
"tax": 0,
"total": 0
}
]
}
]
}
}
Example cURL:
curl -X GET "https://admin.labx.com/api/v1/orders/purchases/12345" \
-H "accept: application/json" \
-H "seller-api-token: Bearer ${LABX_API_TOKEN}"
Error Responses (All Authenticated Endpoints)
401 Unauthorized:
{
"success": false,
"message": "string",
"context": {}
}
429 Too Many Requests:
{
"success": false,
"message": "string",
"context": {}
}
Important API Behaviors (Jesse Brito Clarifications, Feb 2026)
URL Path
- Always use
/api/v1/prefix (documented path) going forward - Legacy code may have used different paths but standardize on this
Token Lifecycle
- Active for the length of ALT's listing program
- Same token survives program renewals
- Will not work if ALT has no active program
- Contact Jesse Brito to rotate if compromised
Upload Behavior
unique_idmatch → updates existing listing- New
unique_id→ creates new listing - Test mode (
testMode: true) not fully supported yet — use production with caution
Field Type Handling
- GET returns
unique_idas integer, POST accepts string - GET returns
listing_activeas 0/1, POST expects boolean - Condition is CASE-SENSITIVE: GET returns Titlecase (e.g., "Refurbished"), POST expects UPPERCASE (e.g., "REFURBISHED")
Rate Limits
- Package listing limit enforced over 7-day rolling window (15,000 listings for LabVista tier)
- No per-request rate limits documented
- For large bulk uploads, notify LabX team in advance
Categories and Manufacturers
- Use lookup endpoints (
GET /api/v1/categories,GET /api/v1/manufacturers) instead of hardcoding values - Category and manufacturer IDs/names may change — always fetch fresh data
Deletion
- No permanent delete via API
- Only deactivation supported via
listing_active: false(POST) oractive: falsein request body
Pricing
- No LabX-side price limit enforced
- "Request a quote" button is optional strategy for high-value items (controlled via
show_quote_buttonfield)
Shipping
- CALCULATED shipping works with FedEx/UPS/USPS carriers based on weight
- Shipping carrier rates configured in LabX Control Panel > Settings > Shipping
- FLAT shipping requires manual price specification
GET page_length
- No hard maximum documented
- Values >1000 may timeout on large result sets
- Default is 1000 which is reasonable for most use cases
Listing Versioning (Discovered Feb 2026)
- LabX retains multiple historical versions of every listing — this is a critical architectural behavior
- For ALT's ~15,947 catalog numbers, LabX stores ~28,819 total listing records but only ~16,069 unique
unique_idvalues - That means ~12,750 listings are stale duplicates (5,075 catalog numbers have 2–82 copies each)
- 82 listings have a blank
unique_id(likely test data, including one labeled "API UPDATE TEST") - When fetching via
GET /api/v1/listings, all versions are returned, not just the current one - Deduplication strategy: When comparing or analyzing listings, prefer the active listing per
unique_id, then fall back to the highest SKU as a tiebreaker - Consider requesting LabX API enhancement from Jesse Brito — a way to query only the current/latest version per listing would reduce API calls and improve accuracy
show_quote_button vs show_buy_now_button Behavior
- show_quote_button:
trueif price is $1–$10,500 OR if the item is in stock;falseif out of stock AND price is $0/nil or exceeds $10,500 - show_buy_now_button:
trueif price > $0 and price <= $10,500 AND in stock;falseotherwise - When a listing is deactivated (
listing_active=false), LabX preserves the old button states — it does not reset them - This means inactive listings may show stale
show_quote_button=truevalues from when items were in stock — this is cosmetically harmless since the listings are not visible on the marketplace
Manufacturer Name Mapping
- ALT uses
Manufacturer#corresponding_labx_manufacturer_nameto map internal manufacturer names to LabX-expected names - As of Feb 2026: 2,200 of 2,230 manufacturers have LabX name mappings configured
- Remaining mismatches on LabX are stale data from before mappings were set or before a sync pushed corrected names
- Common stale patterns: "Thermo Fisher Scientific" (LabX) vs "Thermo Scientific" (ALT sends), "HP" vs "Hewlett Packard", "Dynamax" vs "Rainin"
- A full sync would resolve all stale manufacturer names in one pass, but rate limits (15,000/week) currently prevent this
ALT Codebase Integration Map
| Component | File Path | Purpose |
|---|---|---|
| Core sync methods | app/models/model.rb |
Primary LabX integration logic |
| Sync controller | app/controllers/api/labx_sync_controller.rb |
Internal API for triggering sync |
| Background job | app/jobs/sync_labx_job.rb |
Async sync execution with locking |
| Execution tracking | app/models/labx_sync_execution.rb |
Sync run status and metrics |
| Lambda trigger | lambda/labx_sync_trigger.py |
CloudWatch scheduled event handler |
| Rake tasks | lib/tasks/labx_sync.rake |
Manual CLI operations |
| Manufacturer mapper | lib/manufacturer_labx_updater.rb |
CSV-based manufacturer name mapping |
Core Methods in app/models/model.rb
| Method | Purpose | Parameters |
|---|---|---|
synchronize_labx_listings |
Two-phase sync orchestrator (upload + deactivate) | listing_mode, deactivation_mode, days_back, batch_delay |
update_labx_listings |
Format and POST to API with retry/backoff | models (array) |
fetch_labx_listings |
GET all listings with pagination | None |
labx_variance_report |
Compare local vs LabX listings (with dedup) | exclude_fields:, ignore_truncations: |
test_labx_sync |
Diagnostic testing for sync operations | models, dry_run |
format_as_hash_for_labx_api_post |
Instance method: map ALT model to LabX schema | None (instance method) |
calculate_shipping_cost_for_labx |
Shipping fee calculation logic | None (instance method) |
show_quote_button_on_labx |
Determine if quote button should show | None (instance method) |
show_buy_now_button_on_labx |
Determine if buy now button should show | None (instance method) |
labx_image_url |
Generate LabX-compatible image URL | None (instance method) |
labx_item_url |
Generate LabX listing URL | None (instance method) |
Internal API Endpoints (app/controllers/api/labx_sync_controller.rb)
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/api/labx_sync/trigger |
POST | HMAC-SHA256 | Trigger SyncLabxJob with mode parameters |
/api/labx_sync/status |
GET | None | Last sync status + 30-day execution statistics |
Background Job Modes (app/jobs/sync_labx_job.rb)
| Mode | Description |
|---|---|
daily |
Recent in-stock + recent out-of-stock (legacy) |
hybrid |
ALL in-stock + recent out-of-stock (RECOMMENDED) |
full |
ALL in-stock + ALL out-of-stock |
weekly |
Alias for full |
custom |
Configurable days_back parameter |
Job Features:
- SuckerPunch async processing
- Redis + file-based distributed locking
- HMAC-SHA256 authentication for trigger endpoint
- Execution tracking via
LabxSyncExecutionmodel
Field Mapping (ALT to LabX)
| ALT Field | LabX Field | Transformation | Notes |
|---|---|---|---|
alt_catalog_number |
unique_id |
Direct | Primary key for matching existing listings |
manufacturer.name |
manufacturer |
Uses corresponding_labx_manufacturer_name if set |
Association lookup |
name |
model |
Direct | Model name |
manufacturer.name + ' ' + name |
name |
Concatenation | Combined display name |
description |
description |
Direct | Full text description |
current_price |
price |
Direct (if >0 and <=10500) | Price capped at $10,500 internally |
shipping_weight_in_grams |
weight |
÷ 1000 | Convert grams to kilograms |
| (hardcoded) | condition |
Always "REFURBISHED" | Uppercase for POST |
| (hardcoded) | warranty_offered |
Always true |
Boolean |
| (hardcoded) | currency |
Always "USD" | String |
| (hardcoded) | weight_units |
Always "KG" | String |
| (hardcoded) | size_units |
Always "INCHES" | String |
subcategory.name |
categories[0] |
Uses corresponding_labx_subcategory_name if set |
Category mapping |
| (computed) | shipping.carrier |
Always "FLAT" | Flat rate shipping |
| (computed) | shipping.type |
Always "Flat" | Flat rate type |
| (computed) | shipping.country |
Always "US" | US-only shipping |
| (computed) | shipping.price |
See shipping calculation | Dynamic based on weight/type |
Shipping Price Calculation Logic
Implemented in calculate_shipping_cost_for_labx:
def calculate_shipping_cost_for_labx
return 5.50 if accessories? # Accessories: $5.50
return 8.95 if heavy_item? # Heavy items: $8.95
# Standard: 4.9% of price, minimum $0.39
[(current_price * 0.049).round(2), 0.39].max
end
Button Display Logic
Quote Button (show_quote_button_on_labx):
- Show if price > $10,500 or price == 0
- Otherwise hide
Buy Now Button (show_buy_now_button_on_labx):
- Show if price > 0 and price <= $10,500
- Otherwise hide
Database Columns for LabX Mapping
| Table | Column | Purpose |
|---|---|---|
categories |
corresponding_labx_category_name |
Override default category name mapping |
subcategories |
corresponding_labx_subcategory_name |
Override default subcategory name mapping |
manufacturers |
corresponding_labx_manufacturer_name |
Override default manufacturer name mapping |
These columns allow manual mapping overrides when ALT's naming doesn't match LabX's expected values.
Environment Variables
| Variable | Purpose | Required | Example |
|---|---|---|---|
LABX_API_TOKEN |
Bearer token for API auth | Yes | 1Password: op://Development/LabX Seller API Token/credential |
LABX_API_ENABLED |
Feature flag for sync | Yes | 'true' or 'false' |
LABX_SYNC_SECRET |
HMAC shared secret for trigger endpoint | Yes | Random 32+ char string |
NOTIFICATION_EMAIL |
Email for sync error notifications | Yes | Email address |
REDIS_URL |
Distributed lock coordination | Yes | Redis connection URL |
Security Note: NEVER include actual token values in code, logs, or documentation. Always reference via ENV['LABX_API_TOKEN'].
1Password Retrieval: The production token is stored in the Development vault as "LabX Seller API Token". Retrieve with:
op item get "LabX Seller API Token" --vault="Development" --fields credential
Sync Modes Explained
Daily Mode (Legacy)
- In-stock: Recent items only (configurable days_back)
- Out-of-stock: Recent items only (configurable days_back)
- Use case: Daily incremental sync for active inventory changes
Hybrid Mode (RECOMMENDED)
- In-stock: ALL items (comprehensive)
- Out-of-stock: Recent items only (configurable days_back, typically 7-30 days)
- Use case: Default daily sync - ensures all in-stock items are visible while cleaning up recent deactivations
- Rationale: Best balance of completeness and performance
Full/Weekly Mode
- In-stock: ALL items
- Out-of-stock: ALL items
- Use case: Weekly comprehensive sync or recovery from sync issues
- Caution: Can be slow and may hit rate limits on large catalogs
Custom Mode
- In-stock: Configurable days_back
- Out-of-stock: Configurable days_back
- Use case: Specific operational needs or testing
Common Operations Quick Reference
Manual Sync Operations
# Trigger daily sync (recent changes)
rails labx_sync:run
# Trigger hybrid sync (all in-stock + recent out-of-stock)
rails labx_sync:sync_async[hybrid,30]
# Trigger full sync (all items)
rails labx_sync:sync_all
# Check last sync status
curl http://localhost:3000/api/labx_sync/status
Testing and Diagnostics
# Test sync connection (dry run)
Model.test_labx_sync(Model.where(in_stock: true).limit(10), dry_run: true)
# Fetch current LabX listings
labx_listings = Model.fetch_labx_listings
puts "Total LabX listings: #{labx_listings.dig('data', 'meta', 'total_records')}"
# Run variance report (compare local vs LabX)
variance = Model.labx_variance_report
puts "Local only: #{variance[:local_only].count}"
puts "LabX only: #{variance[:labx_only].count}"
puts "Price mismatches: #{variance[:price_mismatches].count}"
Triggering Sync via Internal API
# Generate HMAC signature
TIMESTAMP=$(date +%s)
MESSAGE="POST/api/labx_sync/trigger${TIMESTAMP}"
SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$LABX_SYNC_SECRET" | awk '{print $2}')
# Trigger sync
curl -X POST "http://localhost:3000/api/labx_sync/trigger" \
-H "Content-Type: application/json" \
-H "X-Signature: $SIGNATURE" \
-H "X-Timestamp: $TIMESTAMP" \
-d '{"mode": "hybrid", "days_back": 30}'
Checking Sync Execution History
# Last 10 sync executions
LabxSyncExecution.order(created_at: :desc).limit(10).each do |exec|
puts "#{exec.created_at}: #{exec.status} - #{exec.message}"
end
# Stats for last 30 days
recent = LabxSyncExecution.where('created_at > ?', 30.days.ago)
puts "Total executions: #{recent.count}"
puts "Completed: #{recent.where(status: 'completed').count}"
puts "Failed: #{recent.where(status: 'failed').count}"
Best Practices
- Use Hybrid Mode for Daily Sync: Best balance of completeness and performance
- Monitor Execution Status: Check
/api/labx_sync/statusregularly for failures - Respect Rate Limits: Package limit is 15,000 over 7-day window for LabVista tier
- Map Manufacturer Names: Use
corresponding_labx_manufacturer_namefor name discrepancies - Test Before Production Upload: Use
test_labx_syncwithdry_run: true - Never Hard-Delete: Only deactivate listings via
listing_active: false - Case-Sensitive Condition: Always use UPPERCASE for POST (e.g., "REFURBISHED")
- Monitor Variance Reports: Regularly check for discrepancies between local and LabX
- Coordinate Large Uploads: Notify LabX team before uploading >1000 items at once
- Secure Token Storage: Never commit
LABX_API_TOKENto version control
Troubleshooting
Authentication Failures (401)
- Verify
ENV['LABX_API_TOKEN']is set correctly - Check if ALT listing program is active
- Contact Jesse Brito if token rotation needed
Rate Limit Errors (429)
- Check 7-day listing upload count
- Contact LabX for tier upgrade if hitting limits regularly
- Spread large uploads across multiple days
Condition Validation Errors
- Ensure condition is UPPERCASE: "REFURBISHED", not "Refurbished"
- Valid values: NEW, USED, PARTS, REFURBISHED
Sync Job Not Running
- Check Redis connection:
redis-cli ping - Verify
LABX_API_ENABLED='true'in environment - Check SuckerPunch queue:
SuckerPunch::Queue.stats
Variance Report Mismatches
- Run variance report:
Model.labx_variance_report(exclude_fields: [...], ignore_truncations: true) - Deduplication is built in: The report automatically deduplicates LabX listings (prefers active, then highest SKU)
- Use
EXCLUDE_FIELDSenv var for rake task:EXCLUDE_FIELDS=weight,price,description rake labx:variance_report - Use
IGNORE_TRUNCATIONS=1to skip string truncation differences - Boolean fields treat
nilandfalseas equivalent - Check
corresponding_labx_*_namemappings in database for manufacturer/category mismatches - Most name/model/manufacturer mismatches are stale LabX data — a full sync resolves them
show_quote_buttonmismatches on inactive listings are cosmetically harmless — no action needed
Large Page Length Timeouts
- Reduce
page_lengthto 500 or less - Implement pagination for large result sets
- Use
show_activeorshow_inactivefilters to reduce result size
Version History
v1.1.0 (February 23, 2026)
- CRITICAL DISCOVERY: Documented LabX listing versioning behavior — LabX retains multiple historical versions per
unique_id, causing ~12,750 stale duplicates across ~28,819 total records for ALT's ~15,947 catalog numbers - Added deduplication strategy guidance (prefer active listing, then highest SKU)
- Documented
show_quote_buttonvsshow_buy_now_buttonbehavior and stale state preservation on inactive listings - Documented manufacturer name mapping status (2,200/2,230 configured) and common stale mismatch patterns
- Updated
labx_variance_reportmethod documentation with new parameters:exclude_fields:,ignore_truncations: - Enhanced variance report troubleshooting with filtering options and interpretation guidance
- Full variance report results saved at:
docs/labx-variance-report-2026-02-23.md
v1.0.0 (February 2026)
- Initial skill creation
- All 11 endpoints documented
- Jesse Brito clarifications integrated (Feb 2026)
- ALT codebase integration map complete
- Field mappings and transformations documented