Claude-skill-registry-data scaffolding-marketplace-integrations
Generates boilerplate for e-commerce API integrations. Use when the user asks about WooCommerce, Shopify, Bol.com, or Etsy APIs, webhooks, product sync, or order handling.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry-data
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry-data "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/marketplace-integration-helper" ~/.claude/skills/majiayu000-claude-skill-registry-data-scaffolding-marketplace-integrations && rm -rf "$T"
manifest:
data/marketplace-integration-helper/SKILL.mdsource content
Marketplace Integration Helper
When to use this skill
- User asks to integrate with WooCommerce or Shopify
- User needs webhook handlers for orders or products
- User mentions Bol.com, Etsy, or marketplace APIs
- User wants rate-limited API clients
- User asks about product synchronization
Workflow
- Identify target marketplace(s)
- Generate API client with auth
- Add rate limiting and retry logic
- Create webhook handlers
- Add TypeScript types
- Implement error recovery
Instructions
Step 1: Identify Marketplace
| Platform | Auth Type | Rate Limit | Docs |
|---|---|---|---|
| Shopify | OAuth / Access Token | 2 req/sec (burst 40) | Admin API |
| WooCommerce | OAuth 1.0 / API Keys | 25 req/sec | REST API v3 |
| Bol.com | OAuth 2.0 Client Credentials | 25 req/10sec | Retailer API |
| Etsy | OAuth 2.0 PKCE | 10 req/sec | Open API v3 |
Step 2: Base API Client Structure
// lib/marketplace/base-client.ts interface RateLimitConfig { maxRequests: number; windowMs: number; } interface RetryConfig { maxRetries: number; baseDelayMs: number; maxDelayMs: number; } export abstract class BaseMarketplaceClient { protected baseUrl: string; protected rateLimitConfig: RateLimitConfig; protected retryConfig: RetryConfig; private requestQueue: Array<() => Promise<unknown>> = []; private processing = false; constructor( baseUrl: string, rateLimit: RateLimitConfig, retry: RetryConfig = { maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 10000, }, ) { this.baseUrl = baseUrl; this.rateLimitConfig = rateLimit; this.retryConfig = retry; } protected abstract getAuthHeaders(): Record<string, string>; protected async request<T>( method: string, path: string, body?: unknown, ): Promise<T> { return this.enqueue(() => this.executeWithRetry<T>(method, path, body)); } private async executeWithRetry<T>( method: string, path: string, body?: unknown, attempt = 0, ): Promise<T> { try { const response = await fetch(`${this.baseUrl}${path}`, { method, headers: { "Content-Type": "application/json", ...this.getAuthHeaders(), }, body: body ? JSON.stringify(body) : undefined, }); if (response.status === 429) { const retryAfter = parseInt(response.headers.get("Retry-After") || "5"); await this.delay(retryAfter * 1000); return this.executeWithRetry(method, path, body, attempt); } if (!response.ok) { throw new MarketplaceError(response.status, await response.text()); } return response.json(); } catch (error) { if (attempt < this.retryConfig.maxRetries && this.isRetryable(error)) { const delay = Math.min( this.retryConfig.baseDelayMs * Math.pow(2, attempt), this.retryConfig.maxDelayMs, ); await this.delay(delay); return this.executeWithRetry(method, path, body, attempt + 1); } throw error; } } private isRetryable(error: unknown): boolean { if (error instanceof MarketplaceError) { return [408, 429, 500, 502, 503, 504].includes(error.status); } return error instanceof TypeError; // Network errors } private enqueue<T>(fn: () => Promise<T>): Promise<T> { return new Promise((resolve, reject) => { this.requestQueue.push(async () => { try { resolve(await fn()); } catch (e) { reject(e); } }); this.processQueue(); }); } private async processQueue(): Promise<void> { if (this.processing) return; this.processing = true; while (this.requestQueue.length > 0) { const batch = this.requestQueue.splice( 0, this.rateLimitConfig.maxRequests, ); await Promise.all(batch.map((fn) => fn())); if (this.requestQueue.length > 0) { await this.delay(this.rateLimitConfig.windowMs); } } this.processing = false; } private delay(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } } export class MarketplaceError extends Error { constructor( public status: number, public body: string, ) { super(`Marketplace API error ${status}: ${body}`); } }
Step 3: Platform-Specific Clients
Shopify Client:
// lib/marketplace/shopify-client.ts import { BaseMarketplaceClient } from "./base-client"; interface ShopifyProduct { id: number; title: string; body_html: string; vendor: string; product_type: string; variants: ShopifyVariant[]; images: ShopifyImage[]; status: "active" | "archived" | "draft"; } interface ShopifyVariant { id: number; product_id: number; title: string; price: string; sku: string; inventory_quantity: number; } interface ShopifyImage { id: number; src: string; alt: string | null; } interface ShopifyOrder { id: number; order_number: number; email: string; financial_status: string; fulfillment_status: string | null; line_items: ShopifyLineItem[]; total_price: string; currency: string; } interface ShopifyLineItem { id: number; product_id: number; variant_id: number; quantity: number; price: string; } export class ShopifyClient extends BaseMarketplaceClient { private accessToken: string; constructor(shop: string, accessToken: string) { super(`https://${shop}.myshopify.com/admin/api/2024-01`, { maxRequests: 2, windowMs: 1000, }); this.accessToken = accessToken; } protected getAuthHeaders(): Record<string, string> { return { "X-Shopify-Access-Token": this.accessToken }; } // Products async getProducts(limit = 50): Promise<ShopifyProduct[]> { const data = await this.request<{ products: ShopifyProduct[] }>( "GET", `/products.json?limit=${limit}`, ); return data.products; } async getProduct(id: number): Promise<ShopifyProduct> { const data = await this.request<{ product: ShopifyProduct }>( "GET", `/products/${id}.json`, ); return data.product; } async createProduct( product: Partial<ShopifyProduct>, ): Promise<ShopifyProduct> { const data = await this.request<{ product: ShopifyProduct }>( "POST", "/products.json", { product }, ); return data.product; } async updateProduct( id: number, product: Partial<ShopifyProduct>, ): Promise<ShopifyProduct> { const data = await this.request<{ product: ShopifyProduct }>( "PUT", `/products/${id}.json`, { product }, ); return data.product; } // Inventory async updateInventory( inventoryItemId: number, locationId: number, quantity: number, ): Promise<void> { await this.request("POST", "/inventory_levels/set.json", { inventory_item_id: inventoryItemId, location_id: locationId, available: quantity, }); } // Orders async getOrders(status = "any", limit = 50): Promise<ShopifyOrder[]> { const data = await this.request<{ orders: ShopifyOrder[] }>( "GET", `/orders.json?status=${status}&limit=${limit}`, ); return data.orders; } async fulfillOrder(orderId: number, trackingNumber?: string): Promise<void> { await this.request("POST", `/orders/${orderId}/fulfillments.json`, { fulfillment: { tracking_number: trackingNumber, notify_customer: true, }, }); } }
WooCommerce Client:
// lib/marketplace/woocommerce-client.ts import { BaseMarketplaceClient } from "./base-client"; import crypto from "crypto"; interface WooProduct { id: number; name: string; slug: string; type: "simple" | "variable" | "grouped"; status: "publish" | "draft" | "pending"; sku: string; price: string; regular_price: string; stock_quantity: number | null; images: { id: number; src: string; alt: string }[]; } interface WooOrder { id: number; status: string; currency: string; total: string; billing: WooAddress; shipping: WooAddress; line_items: WooLineItem[]; } interface WooAddress { first_name: string; last_name: string; address_1: string; city: string; postcode: string; country: string; } interface WooLineItem { id: number; product_id: number; quantity: number; total: string; } export class WooCommerceClient extends BaseMarketplaceClient { private consumerKey: string; private consumerSecret: string; constructor(siteUrl: string, consumerKey: string, consumerSecret: string) { super(`${siteUrl}/wp-json/wc/v3`, { maxRequests: 25, windowMs: 1000 }); this.consumerKey = consumerKey; this.consumerSecret = consumerSecret; } protected getAuthHeaders(): Record<string, string> { const auth = Buffer.from( `${this.consumerKey}:${this.consumerSecret}`, ).toString("base64"); return { Authorization: `Basic ${auth}` }; } // Products async getProducts(page = 1, perPage = 100): Promise<WooProduct[]> { return this.request("GET", `/products?page=${page}&per_page=${perPage}`); } async getProduct(id: number): Promise<WooProduct> { return this.request("GET", `/products/${id}`); } async createProduct(product: Partial<WooProduct>): Promise<WooProduct> { return this.request("POST", "/products", product); } async updateProduct( id: number, product: Partial<WooProduct>, ): Promise<WooProduct> { return this.request("PUT", `/products/${id}`, product); } async updateStock(productId: number, quantity: number): Promise<WooProduct> { return this.updateProduct(productId, { stock_quantity: quantity }); } // Orders async getOrders(status?: string, page = 1): Promise<WooOrder[]> { const query = status ? `&status=${status}` : ""; return this.request("GET", `/orders?page=${page}${query}`); } async updateOrderStatus(orderId: number, status: string): Promise<WooOrder> { return this.request("PUT", `/orders/${orderId}`, { status }); } }
See examples/bol-etsy-clients.md for Bol.com and Etsy implementations.
Step 4: Webhook Handlers
Webhook verification and routing:
// lib/marketplace/webhooks.ts import crypto from "crypto"; interface WebhookHandler<T = unknown> { topic: string; handler: (payload: T) => Promise<void>; } // Shopify webhook verification export function verifyShopifyWebhook( body: string, hmacHeader: string, secret: string, ): boolean { const hash = crypto .createHmac("sha256", secret) .update(body, "utf8") .digest("base64"); return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(hmacHeader)); } // WooCommerce webhook verification export function verifyWooCommerceWebhook( body: string, signature: string, secret: string, ): boolean { const hash = crypto .createHmac("sha256", secret) .update(body, "utf8") .digest("base64"); return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(signature)); } // Generic webhook router export class WebhookRouter { private handlers: Map<string, WebhookHandler["handler"]> = new Map(); register<T>(topic: string, handler: (payload: T) => Promise<void>): void { this.handlers.set(topic, handler as WebhookHandler["handler"]); } async route(topic: string, payload: unknown): Promise<void> { const handler = this.handlers.get(topic); if (!handler) { console.warn(`No handler registered for topic: ${topic}`); return; } await handler(payload); } }
Next.js API route example:
// app/api/webhooks/shopify/route.ts import { NextRequest, NextResponse } from "next/server"; import { verifyShopifyWebhook, WebhookRouter, } from "@/lib/marketplace/webhooks"; import { handleOrderCreated, handleProductUpdated, } from "@/lib/marketplace/handlers"; const router = new WebhookRouter(); router.register("orders/create", handleOrderCreated); router.register("products/update", handleProductUpdated); export async function POST(request: NextRequest) { const body = await request.text(); const hmac = request.headers.get("X-Shopify-Hmac-Sha256") || ""; const topic = request.headers.get("X-Shopify-Topic") || ""; if (!verifyShopifyWebhook(body, hmac, process.env.SHOPIFY_WEBHOOK_SECRET!)) { return NextResponse.json({ error: "Invalid signature" }, { status: 401 }); } try { await router.route(topic, JSON.parse(body)); return NextResponse.json({ success: true }); } catch (error) { console.error("Webhook processing error:", error); return NextResponse.json({ error: "Processing failed" }, { status: 500 }); } }
Webhook handler implementations:
// lib/marketplace/handlers.ts import { db } from "@/lib/db"; interface ShopifyOrderPayload { id: number; order_number: number; email: string; line_items: Array<{ product_id: number; variant_id: number; quantity: number; }>; } export async function handleOrderCreated( payload: ShopifyOrderPayload, ): Promise<void> { // Idempotency check const existing = await db.order.findUnique({ where: { externalId: `shopify_${payload.id}` }, }); if (existing) return; // Create local order record await db.order.create({ data: { externalId: `shopify_${payload.id}`, platform: "shopify", orderNumber: String(payload.order_number), customerEmail: payload.email, status: "pending", lineItems: { create: payload.line_items.map((item) => ({ externalProductId: String(item.product_id), externalVariantId: String(item.variant_id), quantity: item.quantity, })), }, }, }); // Sync inventory across platforms for (const item of payload.line_items) { await syncInventoryAcrossPlatforms(item.product_id, -item.quantity); } } export async function handleProductUpdated(payload: { id: number; title: string; }): Promise<void> { await db.product.updateMany({ where: { externalId: `shopify_${payload.id}` }, data: { title: payload.title, updatedAt: new Date() }, }); }
Step 5: Product Sync Service
// lib/marketplace/sync-service.ts import { ShopifyClient } from "./shopify-client"; import { WooCommerceClient } from "./woocommerce-client"; interface NormalizedProduct { sku: string; title: string; description: string; price: number; quantity: number; images: string[]; } export class ProductSyncService { constructor( private shopify: ShopifyClient, private woocommerce: WooCommerceClient, ) {} async syncProductToAll(product: NormalizedProduct): Promise<void> { const results = await Promise.allSettled([ this.syncToShopify(product), this.syncToWooCommerce(product), ]); for (const result of results) { if (result.status === "rejected") { console.error("Sync failed:", result.reason); } } } private async syncToShopify(product: NormalizedProduct): Promise<void> { const existing = await this.findShopifyProductBySku(product.sku); if (existing) { await this.shopify.updateProduct(existing.id, { title: product.title, body_html: product.description, variants: [{ ...existing.variants[0], price: String(product.price) }], }); } else { await this.shopify.createProduct({ title: product.title, body_html: product.description, variants: [{ sku: product.sku, price: String(product.price) }] as any, images: product.images.map((src) => ({ src })) as any, }); } } private async syncToWooCommerce(product: NormalizedProduct): Promise<void> { // Similar pattern for WooCommerce } private async findShopifyProductBySku(sku: string) { const products = await this.shopify.getProducts(250); return products.find((p) => p.variants.some((v) => v.sku === sku)); } }
Environment Setup
# .env.local SHOPIFY_SHOP=your-store SHOPIFY_ACCESS_TOKEN=shpat_xxxxx SHOPIFY_WEBHOOK_SECRET=xxxxx WOOCOMMERCE_URL=https://your-store.com WOOCOMMERCE_KEY=ck_xxxxx WOOCOMMERCE_SECRET=cs_xxxxx BOL_CLIENT_ID=xxxxx BOL_CLIENT_SECRET=xxxxx ETSY_API_KEY=xxxxx ETSY_SHARED_SECRET=xxxxx
Validation
Before completing:
- API client handles rate limits correctly
- Webhook signatures verified before processing
- Idempotency checks prevent duplicate processing
- Error recovery with exponential backoff works
- TypeScript types match API responses
Error Handling
- Rate limit exceeded: Queue requests and respect Retry-After header.
- Authentication failure: Check token expiry; refresh OAuth tokens if needed.
- Webhook signature mismatch: Reject immediately; log for investigation.
- Partial sync failure: Use Promise.allSettled; continue with working platforms.
- Network timeout: Retry with exponential backoff up to max retries.