PleasureStock API v2

B2B integration & purchasing-intelligence data exchange

🤖 AI-optimized version — single page for LLMs & automation

🚀 Quick Start Guide

Base URL for API calls: https://pleasurestock.com/api/v1
⚠️ This is not a web page - use this URL in your code for API requests

Available APIs

Rate Limits

External API requests are limited per API key: 1,000 requests/hour for all endpoints, and 60 requests/hour for order creation (POST /api/v1/external/orders). Every response carries standard RateLimit-* headers; exceeding a limit returns 429 with {"success":false,"error":{"code":"RATE_LIMIT_EXCEEDED"}}. Implement exponential backoff and respect the headers.

🔐 Authentication

Getting Your API Key

  1. Login to PleasureStock at https://pleasurestock.com
  2. Navigate to Settings → API Keys
  3. Click "Generate New Key"
  4. Save your key immediately (shown only once!)

Authentication Methods

Method 1: X-API-Key Header (Standard API)

HTTP Header
X-API-Key: pk_live_a1b2c3d4e5f6g7h8i9j0

Method 2: Bearer Token (External API)

HTTP Header
Authorization: Bearer pk_live_a1b2c3d4e5f6g7h8i9j0
💡 Same Key, Different Format: Both methods use the same API key. The only difference is the header format:
  • Standard/Sales API: X-API-Key: YOUR_KEY
  • External API: Authorization: Bearer YOUR_KEY
You do NOT need separate credentials for External API.
Security Notice: Never expose your API key in client-side code or public repositories.

📦 Standard API Endpoints

GET /api/v1/products Stable

Retrieve product catalog with filtering and pagination.

✨ Updated 2025-11-10: Now supports Category, TCC (Total Current Cost), and TSP (Total Sale Price) fields!

Query Parameters

Parameter Type Required Description
page integer No Page number (default: 1)
limit integer No Items per page (max: 1000, default: 100)
supplier_id integer No Filter by supplier ID
search string No Search by name, SKU, or description
withImages boolean No Include product images (default: false)
includeCategory boolean No NEW: Include category information (default: false)
includeTcc boolean No NEW: Include TCC (Total Current Cost) calculations (default: false)
includeTsp boolean No NEW: Include TSP (Total Sale Price) calculations (default: false)

Response Example (Basic)

200 OK
{
  "success": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "internal_id": "ps_vj31ucxsab",
      "name": "Premium Silicone Vibrator",
      "sku": "VIB-001",
      "price": "49.99",
      "currency": "USD",
      "supplier_id": "61dcd6b4-58e8-4945-85dc-b00a35db3a72",
      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-03-20T14:45:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 100,
    "total": 1250,
    "pages": 13
  }
}

Response Example (With Category, TCC, TSP)

GET /api/v1/products?includeCategory=true&includeTcc=true&includeTsp=true
{
  "success": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "internal_id": "ps_vj31ucxsab",
      "name": "Premium Silicone Vibrator",
      "sku": "VIB-001",
      "price": "49.99",
      "currency": "USD",
      "supplier_id": "61dcd6b4-58e8-4945-85dc-b00a35db3a72",

      // Category fields (includeCategory=true) — single-axis taxonomy ("what it is")
      "category_id": "c4f3e2d1-a2b3-4c5d-8e7f-8a9b0c1d2e3f",
      "category_name": "Vibrators",
      "category_name_en": "Vibrators",

      // TCC fields (includeTcc=true) — landed cost, computed in TRY
      "tcc_enabled": true,
      "tcc_coefficient": 2,
      "tcc_total_cost": 748.98,
      "tcc_currency": "TRY",

      // TSP fields (includeTsp=true) — target selling price, in TRY
      "tsp_enabled": true,
      "tsp_value": 1976.46,
      "tsp_formula": "TCC * 2.5 + 104",
      "tsp_currency": "TRY",

      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-03-20T14:45:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 100,
    "total": 1250,
    "pages": 13
  }
}

Field Explanations

Field Description
category_id UUID of the product category
category_name Category display name (English; equals category_name_en)
category_name_en Category name in English
tcc_enabled Whether TCC is enabled for your company
tcc_coefficient Multiplier for calculating total cost (e.g., 1.7 = +70% for shipping, taxes, customs)
tcc_total_cost Total Current Cost = price × coefficient (your actual procurement cost)
tsp_enabled Whether TSP is enabled for your company
tsp_value Total Sale Price - recommended retail price calculated from TCC using your formula
tsp_formula Your pricing formula (e.g., "TCC * 1.5", "TCC + 50")
📘 What are TCC and TSP?

TCC (Total Current Cost) - Your actual procurement cost including supplier price, shipping, taxes, and customs.
Configure in: Settings → TCC Settings

TSP (Total Sale Price) - Your recommended retail price calculated using a formula based on TCC.
Configure in: Settings → TSP Settings

Note: TCC and TSP are company-specific. Each API key sees their own company's calculations.

Usage Examples

Get products with categories only
curl -X GET 'https://pleasurestock.com/api/v1/products?page=1&limit=100&includeCategory=true' \
  -H 'X-API-Key: your-api-key-here'
Get products with pricing calculations
curl -X GET 'https://pleasurestock.com/api/v1/products?page=1&limit=100&includeTcc=true&includeTsp=true' \
  -H 'X-API-Key: your-api-key-here'
Get everything (category + TCC + TSP)
curl -X GET 'https://pleasurestock.com/api/v1/products?page=1&limit=100&includeCategory=true&includeTcc=true&includeTsp=true' \
  -H 'X-API-Key: your-api-key-here'
✨ New in 2025-11-11: Batch query support and single product endpoint!

🚀 Batch Query - Get Specific Products by IDs

Instead of fetching all products, you can request specific products using the ids parameter. This dramatically reduces API response size and improves performance.

Get specific products by IDs (batch query)
curl -X GET 'https://pleasurestock.com/api/v1/products?ids=ps_vj31ucxsab,ps_w2as53xl1z,ps_j2y58ajsit&includeCategory=true&includeTcc=true&includeTsp=true' \
  -H 'X-API-Key: your-api-key-here'
💡 Batch Query Benefits:
Performance: Request only products you need (26 products instead of 2490)
Bandwidth: Reduce API response size by 99%
Flexibility: Mix UUID and ps_* IDs in the same request
Format: Comma-separated list of IDs: ?ids=id1,id2,id3

Parameters

Parameter Type Description
ids string Optional. Comma-separated list of product IDs. Supports both UUID format (db57e260-b151-4bf1-ac63-175baef9f96d) and internal ID format (ps_vj31ucxsab). When provided, only returns specified products.
GET /api/v1/products/:id Stable

Get a single product by its ID (UUID or ps_* internal ID).

✨ New in 2025-11-11: Retrieve individual products without fetching the entire catalog!

URL Parameters

Parameter Type Description
id string Required. Product ID in UUID format (db57e260-b151-4bf1-ac63-175baef9f96d) or internal ID format (ps_vj31ucxsab).

Query Parameters

Parameter Type Default Description
withImages boolean true Include product images
includeCategory boolean false Include category information
includeTcc boolean false Include Total Current Cost calculations
includeTsp boolean false Include Total Sale Price calculations

Usage Examples

Get single product by ps_* ID
curl -X GET 'https://pleasurestock.com/api/v1/products/ps_vj31ucxsab?includeCategory=true&includeTcc=true&includeTsp=true' \
  -H 'X-API-Key: your-api-key-here'
Get single product by UUID
curl -X GET 'https://pleasurestock.com/api/v1/products/db57e260-b151-4bf1-ac63-175baef9f96d?includeCategory=true&includeTcc=true&includeTsp=true' \
  -H 'X-API-Key: your-api-key-here'

Response Example

Single Product Response
{
  "success": true,
  "data": {
    "id": "db57e260-b151-4bf1-ac63-175baef9f96d",
    "internal_id": "ps_vj31ucxsab",
    "name": "Premium Silicone Vibrator",
    "sku": "SL2505044-XCM",
    "price": "8.00",
    "currency": "USD",
    "supplier_id": "f0229142-b869-42c6-8751-9cb3d629e192",
    "created_at": "2025-11-04T08:34:35.815Z",
    "updated_at": "2025-11-04T08:45:47.291Z",

    // Category fields (includeCategory=true) — single-axis taxonomy ("what it is")
    "category_id": "61dcd6b4-58e8-4945-85dc-b00a35db3a72",
    "category_name": "Vibrators",
    "category_name_en": "Vibrators",

    // NOTE: v1 returns category only. Product FEATURES (Rabbit, Thrusting, Realistic…) are TAGS,
    // available via External API v2: GET /api/v2/external/catalog?include=tags

    // TCC fields (includeTcc=true)
    "tcc_enabled": true,
    "tcc_coefficient": 2,
    "tcc_total_cost": 16,
    "tcc_currency": "TRY",

    // TSP fields (includeTsp=true)
    "tsp_enabled": true,
    "tsp_value": 144,
    "tsp_formula": "TCC * 2.5 + 104",
    "tsp_currency": "TRY"
  }
}
💡 Use Case: Perfect for updating individual product information without fetching the entire catalog.
POST /api/v1/sales Stable

Submit sales data for Smart Reordering AI predictions and inventory tracking.

✨ Updated 2025-12-08: Now uses only UUID format (productId) for product identification.

Request Body

JSON
{
  "sales": [
    {
      "productId": "550e8400-e29b-41d4-a716-446655440000",
      "date": "2025-11-03",
      "quantity": 15,
      "stock_on_hand": 120
    },
    {
      "productId": "550e8400-e29b-41d4-a716-446655440000",
      "date": "2025-11-03",
      "quantity": 8,
      "stock_on_hand": 45
    }
  ]
}

Request Parameters

Field Type Required Format Example
productId string ✅ Yes UUID "550e8400-e29b-41d4-a716-446655440000"
date string ✅ Yes YYYY-MM-DD "2025-11-03"
quantity integer ✅ Yes ≥ 0 15
stock_on_hand integer ❌ No ≥ 0 120

Validation Rules

  • productId accepts only UUID format:
    • UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (36 characters)
  • date must be valid ISO date (YYYY-MM-DD format)
  • quantity must be non-negative integer
  • stock_on_hand is optional, non-negative integer
  • Array sales can contain up to 1000 records per request

Success Response

200 OK
{
  "success": true,
  "message": "Sales data submitted successfully",
  "processed": 2
}

Error Response

400 Bad Request
{
  "error": "Validation errors",
  "errors": [
    {
      "index": 0,
      "error": "Invalid productId format",
      "expected": "UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "received": "invalid_id_123"
    }
  ],
  "processed": 0
}
💡 How to find productId (UUID):
1. Login to PleasureStock
2. Go to Products section
3. Find your product by SKU
4. Column "PleasureStock ID" shows the productId (UUID)

What Happens After Submission?

  1. ✅ Sales data is stored in PleasureStock
  2. 🤖 AI automatically calculates Smart Reordering recommendations (once daily)
  3. 📊 View recommendations at: Cart → Smart Reordering tab
  4. 🎯 See recommended quantities, current stock, and forecast accuracy

Best Practices

  • 📅 Frequency: Send data daily (minimum) or every 2-4 hours (optimal)
  • 📊 Historical Data: Submit past 30-90 days for better AI predictions
  • 📦 Batch Size: Up to 1000 records per request
  • 🎯 Accuracy: Always include stock_on_hand for best results
🌍 Idempotency (World-Class Best Practice):
You can safely send the same data multiple times - the system automatically deduplicates by date.

Example: If you send data for 2025-11-03 twice, the system will aggregate:
  • quantitySUM (total sales for the day)
  • stock_on_handMAX (highest stock level = total across warehouses)
This design follows Amazon MWS and Shopify API patterns.

Multiple Warehouses Support

If you have multiple warehouses, send data from each warehouse separately. The system will aggregate automatically:

Multiple Warehouses Example
{
  "sales": [
    {
      "productId": "550e8400-e29b-41d4-a716-446655440000",
      "date": "2025-11-03",
      "quantity": 10,          // Warehouse A sales
      "stock_on_hand": 500     // Warehouse A stock
    },
    {
      "productId": "550e8400-e29b-41d4-a716-446655440000",  // Same product, different warehouse
      "date": "2025-11-03",
      "quantity": 5,           // Warehouse B sales
      "stock_on_hand": 300     // Warehouse B stock
    }
  ]
}

Result: System aggregates to quantity: 15 (total sales) and stock_on_hand: 500 (max = total stock across all warehouses).

Out-of-Stock Days

When a product is out of stock, send stock_on_hand: 0. The AI will:

  • ✅ Exclude these days from average sales calculations
  • ✅ Mark them as OOS (Out-of-Stock) periods
  • ✅ Provide more accurate forecasts by ignoring zero-sales due to stockouts
Out-of-Stock Example
{
  "sales": [{
    "productId": "550e8400-e29b-41d4-a716-446655440000",
    "date": "2025-11-03",
    "quantity": 0,           // No sales possible
    "stock_on_hand": 0       // ⚠️ Out of stock!
  }]
}
GET /api/v1/smart-reordering Stable

Get AI-powered inventory recommendations based on sales history.

Query Parameters

Parameter Type Required Description
date string No Target date for predictions (default: today)
Note: Requires sales data submission for accurate predictions.
GET /api/v1/currency/convert-to-try Stable

Convert prices to Turkish Lira with real-time exchange rates.

Query Parameters

Parameter Type Required Description
amount number Yes Amount to convert
currency string Yes Source currency (EUR, USD, GBP)

🔗 External B2B Integration API

API v2 — Purchasing-intelligence data exchange. Feed us your sales, stock, supply costs and delivery dates; read back what to reorder, how much, by which shipping mode (sea/air), how urgently, and the real landed cost. Full client manual: docs/api/CLIENT_INTEGRATION_MANUAL.md.

Base URL: https://pleasurestock.com/api/v2/external
Auth: Authorization: Bearer YOUR_API_KEY (company-scoped; same key as Standard API)
Envelope: { "success": true, "data": {…} } / { "success": false, "error": "…", "details": [] }
Product key: everything is keyed by our catalog productId (UUID) — map your SKUs first.
Number types: monetary/fractional DECIMAL fields (price, amount, averageDailySales, reorderPoint, daysUntilStockout…) come back as JSON strings — parse as decimal, not blindly as float.
🏷️ Categories & Tags. Our taxonomy is a single axis: one category per product answering "what is this thing" (~30 top-level categories, no sub-categories). Everything about "what it's like" lives in tags, grouped by form (Rabbit, Bullet, Wand, Suction…), style (Realistic, Fantasy…), zone (Clitoral, G-spot…), medium (Water-based), motion (Thrusting), wear (Wearable, Strapless…) and segment (Pocket, Training kit, Accessories).

Example. A rabbit vibrator is category vibrators with tags Rabbit + Thrustingnot a "Rabbit" category. Former sub-categories (Rabbit, Realistic, Water-based, Pocket…) are now tags. When mapping, match on the stable categorySlug, never on the display category name.
💡 Idempotent & recalculating. Every POST is idempotent — re-sending the same keys updates the record and re-triggers calculations (landed cost, recommendations). Your "refresh" = re-POST with new values. Never duplicates, never loses history.

1) Product mapping

GET /api/v2/external/catalog?limit=100&offset=0 Stable

Paginated export of the catalog visible to you → map your SKUs to our productId. limit ≤ 500.

include (optional): comma-separated extras for a rich projection — description (supplier-sourced product text, cleaned & translated to English — raw material for writing your own selling copy), tags (our classification: a controlled vocabulary assigned by our AI consensus pipeline — structured signal that is not derivable from the description), attributes (material, waterproof as an IP-class string like "IPX5", vibration/heating booleans, weight, dimensions, units per box, production lead time — any may be null). Default stays lean; unknown values are silently ignored.

Category identity. Every item carries category (human-readable name, not a stable identifier — names are not historically unique) and categorySlug (the stable identifier). Match on categorySlug, not on the display name. The taxonomy is a single axis of ~30 top-level categories; product features (Rabbit, Realistic, Water-based, Pocket…) are exposed as tags, not as sub-categories.

SKU mapping. sku is our internal unique key (it may carry a collision suffix like -v2 that does not exist in the supplier's price list). supplierSku is the original supplier article code with that suffix stripped — map your listing on supplierSku (or barcode when present), not on the raw sku. ?sku= search matches both. productId (UUID) stays the permanent key.

200 OK
GET /api/v2/external/catalog?limit=100&offset=0&include=description,tags,attributes

{ "success": true, "data": { "total": 6140, "limit": 100, "offset": 0,
  "items": [ { "productId": "b147b193-118e-40c6-bbc0-168aaaaf32a0", "sku": "SHD-S392-2",
               "supplierSku": "SHD-S392-2",
               "barcode": null, "name": "Gino-RCT", "price": "12.35",
               "currency": "USD", "supplier": "S-Hande Technology Co.,Ltd",
               "category": "Vibrators", "categorySlug": "vibrators",
               "description": "Premium device crafted from medical-grade silicone and durable ABS…",
               "tags": ["abs", "Dual-motor", "Rabbit", "Remote-control", "silicone", "vibrating"],
               "material": "Medical silicone + ABS", "waterproof": "IPX5", "hasVibration": true,
               "hasHeating": false, "weightKg": "0.090", "dimensionsCm": null,
               "unitsPerBox": null, "productionTimeDays": null } ] } }
// price/weightKg are strings (DECIMAL); barcode = UPC/EAN/GTIN or null (strongest mapping key)
// waterproof = declared IP class as a STRING ("IPX5"), not a boolean; any attribute may be null
// tags are display names, casing is mixed — compare case-insensitively; legacy values may include
// materials (silicone/abs/tpe) — use the `material` attribute for material, ignore unknown values
GET /api/v2/external/products/search?q=&sku=&limit=25&include=

Targeted candidate lookup (name substring / SKU prefix; at least one). Supports the same include parameter as /catalog. Can't map by SKU? Use GET /matches (similarity engine).

2) PUSH — you send us data

POST /api/v2/external/salesFlow C

Raw sales (every sale or daily-grouped). We compute daily volume, weighted price, trend. Append-only, idempotent on (productId,date,marketplace,saleId).

JSON
{ "sales": [ { "productId": "uuid", "date": "2026-06-01", "units": 3,
  "price": 349.90, "currency": "TRY", "marketplace": "trendyol" } ] }
POST /api/v2/external/stockFlow C

Point-in-time stock observations. Stock is a value that changes during the day (inventory counts, re-grading, receipts/write-offs) — each observation is a real fact, not a duplicate. Send an observedAt timestamp; you may send many observations per day. We store all of them (movement history) and never overwrite — "current stock" = the observation with the latest observedAt. (Legacy "one per day, latest wins" is retired.)

Send out_of_stock (boolean) on every item — a direct availability flag (whether the item was buyable, independent of the number). It overrides the quantity when we decide if a day counts toward demand: on_hand>0 + out_of_stock:true (delisted, sitting in the warehouse) → day is not counted; on_hand:0 + out_of_stock:false (re-grading/swap, still selling) → day is counted. If omitted we fall back to guessing (on_hand:0 = out of stock), which is less accurate.

JSON
{ "stock": [ { "productId": "uuid", "observedAt": "2026-06-01T14:30:00Z", "on_hand": 134, "out_of_stock": false } ] }
POST /api/v2/external/orders/:ref/paymentsFlow B

:ref = delivery_id (PS-YYYY-NNNN) or order_number (ORD-…). Cumulative payments → real landed cost (replaces ×1.7 fallback). Idempotent on (orderNumber,paymentId).

JSON
{ "payments": [ { "paymentId": "sap-pay-771", "date": "2026-04-12", "amount": 1000.00,
  "currency": "USD", "kind": "goods_invoice" } ] }
// kind ∈ goods_invoice | freight | customs | vat | insurance | other
PUT /api/v1/external/orders/{orderNumber}/statusFlow A

7-state machine created→part_paid→paid→on_the_way→delivered→accepted (+cancelled). Report transitions as they happen → real per-mode lead time. Served by v1 today (see External API v1 section); a v2 :ref-based variant accepting delivery_id is planned.

JSON
{ "status": "on_the_way", "comment": "left warehouse" }
POST /api/v2/external/productsFlow D

Submit your listing → similarity engine matches it to our catalog (powers GET /matches).

3) READ — you read our calculations

GET /api/v2/external/recommendations

Reorder recommendations per product (quantity, sea/air method, urgency, days-to-stockout, per-mode scenarios). data.count = number of items. shippingReason is a human-readable, currently localized explanation — display it, don't parse it.

200 OK
{ "success": true, "data": { "count": 16, "items": [ { "productId": "uuid",
  "recommendedQuantity": 462, "currentStock": 19, "ordersInTransit": 0,
  "averageDailySales": "5.02", "forecastAccuracy": "27.08", "calculationDate": "2026-07-10",
  "reorderPoint": "480.39", "safetyStock": "24", "demandTrend": "up",
  "recommendedMethod": "air", "daysUntilStockout": "3.8",
  "shippingUrgency": "air_urgent_stockout_risk", "shippingReason": "",
  "shippingScenarios": {
    "air": { "safetyStock": 24, "leadTimeDays": 61, "reorderPoint": 480.39, "recommendedQuantity": 462 },
    "sea": { "safetyStock": 28.47, "leadTimeDays": 98, "reorderPoint": 670.41, "recommendedQuantity": 652 } } } ] } }
GET /api/v2/external/demand/:productId?windowDays=30

Aggregated demand over the window: units summed across marketplaces, weighted-average price, trend.

200 OK
{ "success": true, "data": { "productId": "uuid", "windowDays": 30, "daysWithSales": 0,
  "totalUnits": 0, "avgDailyUnits": 0, "weightedAvgPrice": null, "trend": "flat" } }
GET /api/v2/external/orders/:ref/cost

Landed cost / TCC for an order. source: actual (from your payments) or estimated (coefficient fallback). Also ships the delivery ETA fields (shippingMethod, estimatedArrivalDate, estimatedArrivalBasis) alongside the cost.

200 OK
{ "success": true, "data": { "orderNumber": "PS-2026-0523", "source": "actual",
  "landedCost": 13388.16, "currency": "USD", "orderTotal": 7887.66,
  "shippingMethod": "sea", "estimatedArrivalDate": "2026-06-29", "estimatedArrivalBasis": "shipped",
  "payments": [ { "paymentId": "101", "date": "2026-05-15", "amount": "2500.00", "currency": "USD", "kind": "other" } ] } }
GET /api/v2/external/matches

Similarity-engine candidates mapping your listing (Flow D) → our catalog products. Each candidate carries a score.

200 OK
{ "success": true, "data": { "method": "text-embedding",
  "embeddings": "product-hub:gemini-embedding-001:768",
  "note": "Embedding-ranked candidate matching via product-hub vector-store (logic in PS).",
  "products": [ { "clientProductId": "1626839826", "clientName": "…", "candidates": [
    { "productId": "uuid", "name": "Thrusting Vibrator — 36*36*225", "sku": "SN22A",
      "supplierSku": "SN22A", "price": "8.85", "currency": "USD", "supplier": "Multi Fun co. Ltd", "score": 0.7041 } ] } ] } }

4) PULL feed — you expose, we poll hourly

Alternatively (lowest effort): publish a read-only feed (token-protected); we poll it hourly (configure base URL + token in your cabinet → /settings/feed). Sections: GET /orders, /payments, /sales, /stock, /products (each with a from/to window; /stock takes a date).
GET /orders?from&to (your feed)

Dates power the real lead time (acceptedDate − firstPaymentDate, per mode). Send real YYYY-MM-DD or omit — never a placeholder/now() for a date you don't have.

JSON
{ "orders": [ { "orderRef": "PS-2026-0523", "status": "on_the_way", "supplier": "Yuyang",
  "currency": "USD", "total": 7887.66,
  "lines": [ { "productId": "uuid", "units": 30, "price": 5.29, "currency": "USD" } ],
  "firstPaymentDate": "2026-05-15",   // first payment to supplier (deposit) — lead-time start
  "shippedDate":      "2026-06-12",   // dispatched / in transit
  "acceptedDate":     "2026-06-26"    // received at warehouse — lead-time end
} ] }
Identifiers: an order has two — delivery_id (PS-YYYY-NNNN) and order_number (ORD-…). Order-scoped endpoints match by either. Marketplace commission is configured in your cabinet (per-marketplace), not sent in the feed.

Legacy — External API v1

v1 (/api/v1/external) is legacy — still live for existing integrations. New clients: use v2 above.
Authentication: External API uses Bearer token format with your same API key:
Authorization: Bearer YOUR_API_KEY

Base URL: https://pleasurestock.com/api/v1/external
Rate Limit: 1,000 req/hour per API key (60 req/hour for order creation); RateLimit-* headers on every response, 429 on excess
💡 No separate credentials needed! Use the same API key you use for Sales API, just change the header format from X-API-Key to Authorization: Bearer.
POST /api/v1/external/orders Stable

Create new orders in PleasureStock system. Items are automatically grouped by supplier.

Request Body

JSON
{
  "items": [
    {
      "productId": "550e8400-e29b-41d4-a716-446655440001",
      "quantity": 10,
      "customPrice": 25.50  // optional, uses product price if not provided
    },
    {
      "productId": "550e8400-e29b-41d4-a716-446655440001",
      "quantity": 5
    }
  ],
  "externalOrderId": "ERP-2025-001",  // your internal reference
  "comment": "Urgent order for customer ABC"
}

Response Example

200 OK
{
  "success": true,
  "data": {
    "orders": [
      {
        "orderNumber": "ORD-2025-000123",
        "deliveryId": "PS-2025-0114",
        "supplierId": 12,
        "status": "created",
        "totalAmount": 255.00,
        "currency": "USD",
        "items": 1,
        "createdAt": "2025-01-14T10:00:00Z"
      },
      {
        "orderNumber": "ORD-2025-000124",
        "deliveryId": "PS-2025-0115",
        "supplierId": 15,
        "status": "created",
        "totalAmount": 125.00,
        "currency": "USD",
        "items": 1,
        "createdAt": "2025-01-14T10:00:00Z"
      }
    ],
    "totalOrders": 2,
    "totalAmount": 380.00
  }
}
Important: Items are automatically grouped by supplier. One API call may create multiple orders.
POST /api/v1/external/smart-reorder Stable

Submit comprehensive sales data for AI-powered reordering recommendations.

Request Body

JSON
{
  "products": [
    {
      "productId": "550e8400-e29b-41d4-a716-446655440001",
      "quantity": 15,
      "current_stock": 85,
      "sales_history": [5, 8, 12, 6, 9, 11, 7]
    }
  ]
}

Validation Requirements

  • quantity must be non-negative number
  • current_stock must be non-negative number
  • sales_history must contain at least 7 days of data
GET /api/v1/external/orders Stable

Retrieve all orders created by your API key with filtering options.

Query Parameters

Parameter Type Required Description
status string No Filter by status (created, part_paid, paid, on_the_way, delivered, accepted, cancelled)
limit integer No Results per page (default: 20)
offset integer No Skip records (default: 0)

Response Example

200 OK
{
  "success": true,
  "data": {
    "orders": [
      {
        "orderId": "a9e312b9-d5bb-4961-8db6-7ea1007e672b",
        "orderNumber": "ORD-20251103-78XZ70",
        "deliveryId": "PS-2025-1103/2",
        "status": "created",
        "statusDisplay": {
          "label": "Created",
          "color": "blue",
          "cssClass": "bg-blue-100 text-blue-800"
        },
        "supplier": { "id": "550e8400-e29b-41d4-a716-446655440001", "name": "Example Supplier" },
        "items": [
          {
            "isCustom": false,
            "productId": "b421eb2c-e1ee-4537-9c19-056d63a337c4",
            "sku": "VIB-001",
            "name": "Premium Silicone Vibrator",
            "imageUrl": "https://pleasurestock.com/uploads/products/vib001.jpg",
            "quantity": 5,
            "unitPrice": "29.99",
            "totalPrice": "149.95",
            "currency": "USD"
          }
        ],
        "totalAmount": "5669.57",
        "currency": "USD",
        "shippingMethod": "sea",
        "estimatedArrivalDate": null,
        "estimatedArrivalBasis": null,
        "estimatedDeliveryDate": null,
        "createdAt": "03/11/25"
      },
      {
        "orderId": "c7f45a1e-2b8d-4e6f-9a3c-1d5e8f012ab3",
        "orderNumber": "ORD-20251108-91QW42",
        "deliveryId": "PS-2025-1108/1",
        "status": "on_the_way",
        "statusDisplay": {
          "label": "On the way",
          "color": "purple",
          "cssClass": "bg-purple-100 text-purple-800"
        },
        "supplier": { "id": "550e8400-e29b-41d4-a716-446655440001", "name": "Example Supplier" },
        "items": [
          {
            "isCustom": false,
            "productId": "b421eb2c-e1ee-4537-9c19-056d63a337c4",
            "sku": "VIB-001",
            "name": "Premium Silicone Vibrator",
            "imageUrl": "https://pleasurestock.com/uploads/products/vib001.jpg",
            "quantity": 5,
            "unitPrice": "29.99",
            "totalPrice": "149.95",
            "currency": "USD"
          }
        ],
        "totalAmount": "5669.57",
        "currency": "USD",
        "shippingMethod": "air",
        "estimatedArrivalDate": "2025-12-16",
        "estimatedArrivalBasis": "shipped",
        "estimatedDeliveryDate": "2025-12-16",
        "createdAt": "08/11/25"
      }
    ],
    "pagination": {
      "total": 8,
      "limit": 20,
      "offset": 0
    }
  }
}
💡 Tip: Each line in items[] carries the catalog sku and an absolute public imageUrl (or null if no photo) so you can build product cards directly from the feed — no extra lookup needed. Use the statusDisplay object for color-coded status badges (cssClass uses Tailwind CSS classes). ETA fields (estimatedArrivalDate/estimatedArrivalBasis) ship with every list row; null until the first payment — see the Delivery ETA note below.
GET /api/v1/external/orders/{orderNumber} Stable

Get detailed information about a specific order including all items and custom line items.

Response Example

200 OK
{
  "success": true,
  "data": {
    "orderId": "a9e312b9-d5bb-4961-8db6-7ea1007e672b",
    "orderNumber": "ORD-20251103-78XZ70",
    "deliveryId": "PS-2025-1103/2",
    "externalOrderId": null,
    "status": "created",
    "statusDisplay": {
      "label": "Created",
      "color": "blue",
      "cssClass": "bg-blue-100 text-blue-800"
    },
    "supplier": {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "name": "Example Supplier"
    },
    "hasCustomItems": true,
    "items": [
      {
        "isCustom": false,
        "productId": "b421eb2c-e1ee-4537-9c19-056d63a337c4",
        "sku": "VIB-001",
        "name": "Premium Silicone Vibrator",
        "imageUrl": "https://pleasurestock.com/uploads/products/vib001.jpg",
        "quantity": 5,
        "unitPrice": "29.99",
        "totalPrice": "149.95",
        "currency": "USD",
        "product": {
          "name": "Premium Silicone Vibrator",
          "sku": "VIB-001",
          "imageUrl": "https://pleasurestock.com/uploads/products/vib001.jpg"
        }
      },
      {
        "isCustom": true,
        "productId": null,
        "customSku": "CUSTOM-7788",
        "customName": "OEM packaging insert (client artwork)",
        "customCategory": "Packaging",
        "customDescription": "Printed insert, 90x50mm, matte",
        "imageUrl": null,
        "quantity": 2000,
        "unitPrice": "0.12",
        "totalPrice": "240.00",
        "currency": "USD"
      }
    ],
    "totalAmount": "5669.57",
    "currency": "USD",
    "shippingMethod": "sea",
    "estimatedArrivalDate": "2025-11-20",
    "estimatedArrivalBasis": "first_payment",
    "estimatedDeliveryDate": "2025-11-20",
    "actualDeliveryDate": null,
    "createdAt": "03/11/25",
    "confirmedAt": "03/11/25",
    "paidAt": null,
    "shippedAt": null,
    "acceptedAt": null
  }
}
Delivery ETA: estimatedArrivalDate is the estimated arrival date, based on the median of real deliveries for this shippingMethod (sea/air). Before shipment it is counted from the first payment; after shipment it is refined from the shipment date — estimatedArrivalBasis tells which (first_payment | shipped). It is null until the first payment arrives, and null after acceptance (use acceptedAt for the actual date). estimatedDeliveryDate is a legacy alias of estimatedArrivalDate.
Catalog vs custom line items: every line carries an isCustom boolean.
  • isCustom: false — catalog product: use productId (UUID), sku, name, imageUrl (absolute public URL).
  • isCustom: true — custom line (not in our catalog): productId is null; use customSku, customName, customCategory, customDescription; imageUrl is null.
Switch on isCustom — present on every line in the list, detail and order.updated webhook. Do not infer from productId === null alone.
PUT /api/v1/external/orders/{orderNumber}/status Stable

Update order status in the workflow.

Request Body

JSON
{
  "status": "paid",
  "externalOrderId": "EXT-123456",
  "comment": "Payment confirmed"
}
⚠️ Important Rule: PleasureStock creates orders with status created. All subsequent status updates must come from the client (your system) via this API endpoint. PleasureStock does not change order statuses - you are responsible for updating statuses as the order progresses through your workflow.

Status Workflow

Order Status Lifecycle:
createdpart_paidpaidon_the_waydeliveredaccepted
Cancellation: Can be set from any non-final status → cancelled
Final statuses: accepted, cancelled
Status Set By Description Notifications
created System Order created, awaiting payment None
part_paid External API Partial payment received Email to supplier
paid External API Full payment confirmed Email to supplier
on_the_way External API Shipment dispatched Email to buyer
delivered External API Goods delivered to buyer (awaiting acceptance) Email to buyer and supplier
accepted External API Goods received and accepted by buyer (FINAL) Email to supplier
cancelled System/User Order cancelled (FINAL - cannot be changed) Email to buyer and supplier

📋 Best Practices for Working with Order Statuses

Filtering Active Orders (Exclude Cancelled):
To get only active orders and avoid cancelled duplicates, use status filters:
Get only active orders
GET /api/v1/external/orders?status=created
GET /api/v1/external/orders?status=paid
GET /api/v1/external/orders?status=on_the_way
Handling Duplicate Orders:
If you see multiple orders with identical content and amounts, check the status field:
• Orders with status: "cancelled" are NOT active
• Each order has unique orderId, orderNumber, and deliveryId
• Use deliveryId to identify the latest active order (e.g., PS-2025-1103/2 is newer than PS-2025-1103/1)

Using statusDisplay Field

The API returns a statusDisplay object with formatted status information:

statusDisplay object
{
  "status": "cancelled",
  "statusDisplay": {
    "label": "Cancelled",
    "color": "red",
    "cssClass": "bg-red-100 text-red-800"
  }
}

This field is useful for displaying order status in your UI with appropriate styling.

POST /api/v1/external/webhook Stable

Receive webhook notifications for order events.

Webhook Events

  • order.confirmed - Order confirmed in system
  • order.status_changed - Order status updated

🤖 Connect via MCP

The PleasureStock catalog is available as a remote MCP server (Model Context Protocol), so AI agents — Claude Code, Cursor and any MCP-capable client — can read the catalog as native tools. One config line, your existing API key, no extra setup.

Endpoint: https://pleasurestock.com/mcp  ·  Transport: Streamable HTTP (protocol 2025-11-25), stateless  ·  Auth: Authorization: Bearer YOUR_API_KEY (same key as the REST API)  ·  Access: read-only, free under the shared 1,000 req/hour per-key limit.

Add to Claude Code

shell
claude mcp add --transport http pleasurestock https://pleasurestock.com/mcp \
  --header "Authorization: Bearer YOUR_API_KEY"

Add to Cursor (~/.cursor/mcp.json)

JSON
{
  "mcpServers": {
    "pleasurestock": {
      "url": "https://pleasurestock.com/mcp",
      "headers": { "Authorization": "Bearer YOUR_API_KEY" }
    }
  }
}

Available tools (phase 1 — read-only)

TOOL catalog_search

Search products by name and/or SKU, or browse the catalog with cursor pagination. response_format: concise (default) or detailed.

TOOL catalog_get_product

Full card of one product by its stable productId (UUID): name, description, wholesale price + currency, images, supplier, category, physical attributes and lead time.

TOOL catalog_lookup

Batch-resolve up to 50 products by UUID at once; returns found cards plus a not_found list.

TOOL catalog_list_categories

The flat list of top-level categories to understand the catalog taxonomy. The taxonomy has a single level — children is always an empty array, kept for backward compatibility. Product features (Rabbit, Realistic, Water-based…) are tags, not categories.

Scope: every tool is company-scoped by your key and shows the same catalog as the REST API (active products of active suppliers + your own personal items). Write tools (orders, listing ingest), buyer-recommendation and similarity tools, and OAuth for Claude.ai / ChatGPT connectors are on the phase-2 roadmap.

🔔 Webhooks

Configuration

Outbound order-event webhooks are configured on request — contact [email protected] with your receiving endpoint URL. Self-service webhook management in your account settings is coming soon.

Webhook Events

Event Trigger Payload
order.created New order created via API Full order details with items
order.status_changed Order status updated Order with old/new status
order.cancelled Order cancelled Order details with cancellation reason

Webhook Security

All webhook requests include HMAC-SHA256 signature for verification:

Headers
X-Webhook-Signature: sha256=4f8b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b
X-Webhook-Timestamp: 1736850600

Webhook Payload Examples

order.updated Event

PleasureStock emits a single outbound event — order.updated — whenever an order/delivery changes (status, payment, shipping). It carries the full order snapshot, including items[] with the catalog sku and an absolute public imageUrl (or null if no photo) per line, so you can create/refresh product cards directly from the webhook.

POST to your endpoint
{
  "event": "order.updated",
  "timestamp": "2025-01-14T10:00:00Z",
  "data": {
    "orderId": "550e8400-e29b-41d4-a716-446655440000",
    "orderNumber": "ORD-2025-000123",
    "deliveryId": "PS-2025-0114",
    "externalOrderId": "ERP-2025-001",
    "status": "on_the_way",
    "shippingMethod": "sea",
    "trackingNumber": "TRK-998877",
    "supplier": { "id": "550e8400-e29b-41d4-a716-446655440001", "name": "Example Supplier" },
    "items": [
      {
        "isCustom": false,
        "productId": "b421eb2c-e1ee-4537-9c19-056d63a337c4",
        "sku": "VIB-001",
        "name": "Premium Silicone Vibrator",
        "imageUrl": "https://pleasurestock.com/uploads/products/vib001.jpg",
        "quantity": 10,
        "unitPrice": 25.50,
        "totalPrice": 255.00,
        "currency": "USD"
      }
    ],
    "totalAmount": 255.00,
    "currency": "USD",
    "shippingMethod": "air",
    "estimatedArrivalDate": "2025-02-17",
    "estimatedArrivalBasis": "shipped",
    "estimatedDeliveryDate": "2025-02-17",
    "orderUrl": "https://pleasurestock.com/orders/550e8400-e29b-41d4-a716-446655440000",
    "paidAt": "2025-01-16T14:30:00Z",
    "onTheWayAt": "2025-01-18T09:00:00Z",
    "createdAt": "2025-01-14T10:00:00Z"
  }
}

order.status_changed Event

POST to your endpoint
{
  "event": "order.status_changed",
  "timestamp": "2025-01-14T11:00:00Z",
  "data": {
    "orderId": "550e8400-e29b-41d4-a716-446655440000",
    "orderNumber": "ORD-2025-000123",
    "oldStatus": "created",
    "newStatus": "paid",
    "changedBy": "external_api",
    "comment": "Payment confirmed via bank transfer",
    "changedAt": "2025-01-14T11:00:00Z"
  }
}

Webhook Retry Mechanism

Automatic Retries: Failed webhook deliveries are retried with exponential backoff:
• 1st retry: After 2 seconds
• 2nd retry: After 4 seconds
• 3rd retry: After 8 seconds
• Timeout: 10 seconds per request
• Failed webhooks are queued for manual retry

⚠️ Error Handling

Error Response Format

Error Response
{
  "success": false,
  "error": "Invalid request",
  "message": "The request payload is invalid",
  "details": ["Missing required field: productId"]
}

HTTP Status Codes

Code Description Common Causes
200 Success Request processed successfully
400 Bad Request Invalid parameters, validation errors
401 Unauthorized Missing or invalid API key
403 Forbidden Insufficient permissions
404 Not Found Resource doesn't exist
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Server-side error

💻 Code Examples

cURL

Get Products
curl -X GET "https://pleasurestock.com/api/v1/products?limit=10" \
  -H "X-API-Key: pk_live_your_api_key_here"

Python

Create Order via External API
import requests
import hashlib
import hmac
import time

class PleasureStockAPI:
    def __init__(self, api_key, webhook_secret=None):
        self.api_key = api_key
        self.webhook_secret = webhook_secret
        self.base_url = "https://pleasurestock.com/api/v1"
    
    def create_order(self, items, external_order_id=None, comment=None):
        """Create a new order in PleasureStock"""
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        }
        
        payload = {
            "items": items,
            "externalOrderId": external_order_id,
            "comment": comment
        }
        
        response = requests.post(
            f"{self.base_url}/external/orders",
            json=payload,
            headers=headers
        )
        
        if response.status_code == 200:
            return response.json()
        else:
            raise Exception(f"API Error: {response.text}")
    
    def update_order_status(self, order_number, status, comment=None):
        """Update order status"""
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        }
        
        payload = {"status": status}
        if comment:
            payload["comment"] = comment
        
        response = requests.put(
            f"{self.base_url}/external/orders/{order_number}/status",
            json=payload,
            headers=headers
        )
        
        return response.json()
    
    def verify_webhook(self, signature, timestamp, body):
        """Verify webhook signature"""
        if not self.webhook_secret:
            return False
        
        message = f"{timestamp}.{body}"
        expected = hmac.new(
            self.webhook_secret.encode(),
            message.encode(),
            hashlib.sha256
        ).hexdigest()
        
        return f"sha256={expected}" == signature

# Example usage
api = PleasureStockAPI("pk_live_your_api_key_here")

# Create order
order = api.create_order(
    items=[
        {"productId": "550e8400-e29b-41d4-a716-446655440001", "quantity": 10, "customPrice": 25.50},
        {"productId": "550e8400-e29b-41d4-a716-446655440001", "quantity": 5}
    ],
    external_order_id="ERP-2025-001",
    comment="Urgent order"
)

print(f"Created {order['data']['totalOrders']} orders")
for o in order['data']['orders']:
    print(f"  - {o['orderNumber']}: ${o['totalAmount']}")

# Update status to paid
api.update_order_status(
    order['data']['orders'][0]['orderNumber'],
    "paid",
    "Payment confirmed"
)

Node.js

External API - Get Orders
const axios = require('axios');

class PleasureStockAPI {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseURL = 'https://pleasurestock.com/api/v1';
  }

  async getOrders(status = null) {
    try {
      const params = status ? { status } : {};
      
      const response = await axios.get(
        `${this.baseURL}/external/orders`,
        {
          headers: {
            'Authorization': `Bearer ${this.apiKey}`
          },
          params
        }
      );
      
      return response.data;
    } catch (error) {
      console.error('API Error:', error.response?.data || error.message);
      throw error;
    }
  }

  async updateOrderStatus(orderNumber, status, comment = '') {
    try {
      const response = await axios.put(
        `${this.baseURL}/external/orders/${orderNumber}/status`,
        { status, comment },
        {
          headers: {
            'Authorization': `Bearer ${this.apiKey}`,
            'Content-Type': 'application/json'
          }
        }
      );
      
      return response.data;
    } catch (error) {
      console.error('API Error:', error.response?.data || error.message);
      throw error;
    }
  }
}

// Usage
const api = new PleasureStockAPI('pk_live_your_api_key_here');

// Get pending orders
api.getOrders('pending')
  .then(orders => {
    console.log(`Found ${orders.data.orders.length} pending orders`);
  });

// Update order status
api.updateOrderStatus('ORD-2024-001234', 'paid', 'Payment confirmed')
  .then(result => {
    console.log('Order updated:', result);
  });

🎮 API Playground

Enter your API key to test endpoints directly from this page.

Test API Connection