Etsy Product Sync Implementation
Overview
Complete implementation of a long-running workflow system for syncing MedusaJS products to Etsy, following the person-import pattern with confirmation-based execution.
Architecture
Module: etsysync
Location: src/modules/etsysync/
Models:
-
etsy_account- OAuth credentials and shop informationshop_id,shop_nameaccess_token,refresh_token,token_expires_atapi_config(JSON for additional settings)is_active(boolean)
-
etsy_sync_job- Tracks batch sync operationstransaction_id(workflow transaction ID)status(enum: pending, confirmed, processing, completed, failed)total_products,synced_count,failed_counterror_log(JSON)started_at,completed_at
Module Constant: ETSYSYNC_MODULE = "etsysync"
Module Link: Product ↔ Etsy
File: src/links/product-etsy-link.ts
Links Product (core) with EtsyAccount (etsysync module) using extraColumns to store per-product sync data:
etsy_listing_id- Etsy listing IDetsy_url- Public Etsy listing URLsync_status- Current status (pending, synced, failed, out_of_sync)last_synced_at- Timestamp of last successful syncsync_error- Error message if sync failedmetadata- Additional sync metadata (JSON)
Pattern:
defineLink(
{ linkable: ProductModule.linkable.product, isList: true },
{ linkable: EtsysyncModule.linkable.etsyAccount, isList: false },
{ database: { extraColumns: { ... } } }
)
Workflows
1. Main Long-Running Workflow
File: src/workflows/etsy_sync/workflows/sync-products-to-etsy.ts
ID: "sync-products-to-etsy"
Input:
{
product_ids: string[]
etsy_account_id: string
}
Flow:
-
Create Sync Job (
createEtsySyncJobStep)- Creates
etsy_sync_jobrecord withtransaction_idfrom workflow context - Status:
pending - Stores product count and initializes counters
- Creates
-
Create Product Links (
createProductEtsyLinksStep)- Creates link records for all products with status
pending - Uses module link with extraColumns
- Rollback: dismisses created links
- Creates link records for all products with status
-
Wait for Confirmation (
waitConfirmationEtsySyncStep)- Async step with 1-hour timeout
- Makes this a long-running workflow
- Admin must confirm via API before proceeding
-
Failure Notification (
notifyOnFailureStep)- Sends admin UI feed notification if workflow fails
-
Background Batch Sync (
batchSyncProductsWorkflow.runAsStep)- Runs asynchronously in background
config({ async: true, backgroundExecution: true })- Processes products and updates link records + sync job
-
Success Notification (
sendNotificationsStep)- Sends admin UI feed notification when sync starts
Returns: { total: number }
2. Batch Sync Workflow
File: src/workflows/etsy_sync/workflows/batch-sync-products.ts
ID: "batch-sync-products-to-etsy"
Input:
{
product_ids: string[]
etsy_account_id: string
sync_job_id: string
}
Step: batchSyncProductsStep
Logic:
- Update sync job status to
processing - For each product:
- Call Etsy API (currently stubbed with mock data)
- On success:
- Dismiss old link
- Create new link with:
sync_status: "synced", listing ID, URL, timestamp - Increment
synced_count
- On failure:
- Dismiss old link
- Create new link with:
sync_status: "failed", error message - Increment
failed_count
- Update sync job with final counts and status (
completedorfailed)
Note: Links don't have an update method, so we use dismiss + create pattern.
API Endpoints
1. Start Sync
Route: POST /admin/products/etsy-sync
File: src/api/admin/products/etsy-sync/route.ts
Request Body:
{
product_ids: string[]
etsy_account_id: string
}
Response: 202 Accepted
{
"transaction_id": "wf_01...",
"summary": {
"total": 5
}
}
Flow:
- Validates
product_idsandetsy_account_id - Runs
syncProductsToEtsyWorkflow - Returns transaction ID for confirmation
2. Confirm Sync
Route: POST /admin/products/etsy-sync/{transaction_id}/confirm
File: src/api/admin/products/etsy-sync/[transaction_id]/confirm/route.ts
Response: 200 OK
{
"success": true
}
Flow:
- Resolves
IWorkflowEngineService - Calls
setStepSuccesswith:workflowId: syncProductsToEtsyWorkflowIdstepId: waitConfirmationEtsySyncStepIdtransactionIdfrom URL params
- Unblocks the waiting workflow to proceed with background sync
Workflow Steps
Core Steps
Location: src/workflows/etsy_sync/steps/
-
wait-confirmation-etsy-sync.ts- Async step that pauses workflow execution
- 1-hour timeout
- Unblocked by confirm API
-
create-product-etsy-links.ts- Creates pending link records for all products
- Uses
remoteLink.createwith extraColumns data - Compensation: dismisses created links
-
batch-sync-products.ts- Processes each product sequentially
- Calls Etsy API (stubbed)
- Updates link records via dismiss + create
- Updates sync job counts and status
Usage Flow
Admin Workflow
-
Initiate Sync:
POST /admin/products/etsy-sync
{
"product_ids": ["prod_123", "prod_456"],
"etsy_account_id": "etsy_acc_789"
}Response:
{ transaction_id: "wf_01ABC..." } -
Review & Confirm:
- Admin reviews products to be synced
- Confirms via:
POST /admin/products/etsy-sync/wf_01ABC.../confirm -
Background Processing:
- Workflow proceeds automatically
- Products synced to Etsy
- Link records updated with results
- Admin receives feed notification
-
Check Status:
- Query
etsy_sync_jobby transaction_id - Query product-etsy links for per-product status
- Query
Data Flow
Link Record Lifecycle
-
Created (pending):
{
sync_status: "pending",
etsy_listing_id: null,
etsy_url: null,
last_synced_at: null,
sync_error: null,
metadata: {}
} -
Synced (success):
{
sync_status: "synced",
etsy_listing_id: "etsy_listing_...",
etsy_url: "https://www.etsy.com/listing/...",
last_synced_at: "2025-01-14T12:00:00Z",
sync_error: null,
metadata: {}
} -
Failed:
{
sync_status: "failed",
etsy_listing_id: null,
etsy_url: null,
last_synced_at: null,
sync_error: "API error: ...",
metadata: {}
}
Next Steps
TODO: Etsy API Integration
File to implement: src/modules/etsysync/etsy-api-service.ts
Required methods:
-
createListing(accountId, productData)- POST to Etsy API v3:
/shops/{shop_id}/listings - Maps MedusaJS product to Etsy listing format
- Returns listing ID and URL
- POST to Etsy API v3:
-
updateListing(listingId, productData)- PUT to Etsy API v3:
/listings/{listing_id} - Updates existing listing
- PUT to Etsy API v3:
-
uploadImages(listingId, imageUrls)- POST to Etsy API v3:
/listings/{listing_id}/images - Uploads product images
- POST to Etsy API v3:
-
refreshAccessToken(accountId)- OAuth token refresh logic
- Updates
etsy_accountrecord
Replace stub in batch-sync-products.ts:
// Current stub:
const mockListingId = `etsy_listing_${Date.now()}...`
const mockUrl = `https://www.etsy.com/listing/${mockListingId}`
// Replace with:
const etsyService = container.resolve("etsy-api-service")
const { listingId, url } = await etsyService.createListing(
input.etsy_account_id,
productData
)
TODO: OAuth Flow
Endpoints to add:
-
GET /admin/etsy/auth- Redirects to Etsy OAuth
- Scopes:
listings_w,listings_r,shops_r
-
GET /admin/etsy/callback- Handles OAuth callback
- Exchanges code for tokens
- Creates
etsy_accountrecord
TODO: Validators
File: src/api/admin/products/etsy-sync/validators.ts
import { z } from "zod"
export const AdminSyncProductsToEtsyReq = z.object({
product_ids: z.array(z.string()).min(1),
etsy_account_id: z.string(),
})
Update route to use: req.validatedBody
TODO: UI Components
-
Product List:
- "Sync to Etsy" bulk action
- Sync status badge per product
-
Product Detail:
- Etsy sync status section
- Etsy listing URL link
- Last synced timestamp
- Re-sync button
-
Sync Jobs Dashboard:
- List all sync jobs
- Real-time status updates
- Error logs
- Retry failed syncs
Testing
Manual Test Flow
-
Setup:
# Run migrations
yarn medusa db:migrate
# Create test Etsy account
# (via admin UI or direct DB insert) -
Start Sync:
curl -X POST http://localhost:9000/admin/products/etsy-sync \
-H "Content-Type: application/json" \
-d '{
"product_ids": ["prod_123"],
"etsy_account_id": "etsy_acc_789"
}' -
Confirm:
curl -X POST http://localhost:9000/admin/products/etsy-sync/{transaction_id}/confirm -
Check Results:
- Query
etsy_sync_jobtable - Query product-etsy link table
- Check admin UI feed for notifications
- Query
Key Patterns Used
-
Long-Running Workflow:
- Async step with confirmation
- Background execution
- Transaction ID tracking
-
Module Links with extraColumns:
- Stores relationship data directly on link
- No separate mapping model needed
- Dismiss + create pattern for updates
-
Workflow Composition:
- Main workflow orchestrates
- Batch workflow runs in background
- Proper step isolation
-
Error Handling:
- Per-product error tracking
- Job-level status aggregation
- Feed notifications for admins
Files Created
Module
src/modules/etsysync/index.tssrc/modules/etsysync/service.tssrc/modules/etsysync/models/etsy_account.tssrc/modules/etsysync/models/etsy_sync_job.ts
Link
src/links/product-etsy-link.ts
Workflows
src/workflows/etsy_sync/index.tssrc/workflows/etsy_sync/workflows/sync-products-to-etsy.tssrc/workflows/etsy_sync/workflows/batch-sync-products.tssrc/workflows/etsy_sync/steps/index.tssrc/workflows/etsy_sync/steps/wait-confirmation-etsy-sync.tssrc/workflows/etsy_sync/steps/create-product-etsy-links.tssrc/workflows/etsy_sync/steps/batch-sync-products.ts
API
src/api/admin/products/etsy-sync/route.tssrc/api/admin/products/etsy-sync/[transaction_id]/confirm/route.ts
Configuration
Module Registration
Already added to medusa-config.ts and medusa-config.prod.ts:
modules: [
// ...
{
resolve: "./src/modules/etsysync",
},
]
Link Registration
Links are auto-discovered from src/links/ directory.
Summary
✅ Complete:
- Module with 2 models (etsy_account, etsy_sync_job)
- Product-Etsy link with extraColumns for sync data
- Long-running workflow with confirmation pattern
- Background batch sync workflow
- Admin API endpoints (start + confirm)
- Proper error handling and notifications
⏳ Pending:
- Etsy OAuth flow
- Etsy API service implementation
- Request validators
- Admin UI components
- Integration tests
The core infrastructure is ready. Next step is integrating with Etsy's actual API and building the UI layer.