PleasureStock API — AI-Optimized Documentation

👤 Interactive version for humans (tabs, playground, copy buttons)
Base URL for API calls: https://pleasurestock.com/api/v1
⚠️ Note: This is not a web page URL - use it in your code for API requests
Format: JSON
Rate Limits: 1,000 requests/hour per API key (60/hour for order creation); RateLimit-* headers on every response; 429 + error envelope on excess
Authentication: API Key required (Bearer token for External API)

Authentication

All API requests require authentication using an API key. Get your key from Settings → API Keys in the PleasureStock dashboard.

Standard API Authentication

X-API-Key: pk_live_your_api_key_here

External API Authentication

Authorization: Bearer pk_live_your_api_key_here
IMPORTANT: Same API Key for Both!
Both Standard API and External API 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.

Standard API Endpoints

GET /api/v1/products

Retrieve product catalog with pagination and filtering.

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

Query Parameters:

ParameterTypeRequiredDescription
pageintegerNoPage number (default: 1)
limitintegerNoItems per page (max: 1000, default: 100)
supplier_idintegerNoFilter by supplier ID
searchstringNoSearch by name, SKU, or description
withImagesbooleanNoInclude product images (default: false)
includeCategorybooleanNoNEW: Include category information (default: false)
includeTccbooleanNoNEW: Include TCC calculations (default: false)
includeTspbooleanNoNEW: Include TSP calculations (default: false)

Response Example (Basic):

{
  "success": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "internal_id": "ps_vj31ucxsab",
      "name": "Premium Widget",
      "sku": "WDG-001",
      "price": "29.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):

{
  "success": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "internal_id": "ps_vj31ucxsab",
      "name": "Premium Widget",
      "sku": "WDG-001",
      "price": "29.99",
      "currency": "USD",
      "supplier_id": "61dcd6b4-58e8-4945-85dc-b00a35db3a72",

      // Category fields (includeCategory=true) — single-axis taxonomy
      "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) — 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
  }
}
📘 What are TCC and TSP?
TCC (Total Current Cost) - Your actual procurement cost (supplier price × coefficient for shipping, taxes, customs).
TSP (Total Sale Price) - Your recommended retail price calculated from TCC using your custom formula.
Configure in: Settings → TCC/TSP Settings

Usage examples:
GET /api/v1/products?includeCategory=true - with categories
GET /api/v1/products?includeTcc=true&includeTsp=true - with pricing
GET /api/v1/products?includeCategory=true&includeTcc=true&includeTsp=true - all fields
✨ New in 2025-11-11: Batch Query & Single Product Endpoint

🚀 Batch Query - Get Specific Products by IDs

Request specific products using the ids parameter instead of fetching all products:

GET /api/v1/products?ids=ps_vj31ucxsab,ps_w2as53xl1z,ps_j2y58ajsit&includeCategory=true&includeTcc=true&includeTsp=true
💡 Batch Query Benefits:
Performance: Request only products you need (26 instead of 2490)
Bandwidth: Reduce response size by 99%
Flexibility: Mix UUID and ps_* IDs in the same request
Format: Comma-separated: ?ids=id1,id2,id3

Parameters:

ParameterTypeDescription
idsstringOptional. Comma-separated list of product IDs. Supports UUID (db57e260-b151-4bf1-ac63-175baef9f96d) and ps_* (ps_vj31ucxsab) formats.

GET /api/v1/products/:id

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

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

URL Parameters:

ParameterTypeDescription
idstringRequired. Product ID in UUID format or ps_* format

Query Parameters:

ParameterTypeDefaultDescription
withImagesbooleantrueInclude product images
includeCategorybooleanfalseInclude category information
includeTccbooleanfalseInclude Total Current Cost calculations
includeTspbooleanfalseInclude Total Sale Price calculations

Usage Examples:

GET /api/v1/products/ps_vj31ucxsab?includeCategory=true&includeTcc=true&includeTsp=true
GET /api/v1/products/db57e260-b151-4bf1-ac63-175baef9f96d?includeCategory=true&includeTcc=true&includeTsp=true

Response Example:

{
  "success": true,
  "data": {
    "id": "db57e260-b151-4bf1-ac63-175baef9f96d",
    "internal_id": "ps_vj31ucxsab",
    "sku": "SL2505044-XCM",
    "price": "8.00",
    "currency": "USD",
    "category_id": "61dcd6b4-58e8-4945-85dc-b00a35db3a72",
    "category_name": "Vibrators",
    "category_name_en": "Vibrators",
    "tcc_enabled": true,
    "tcc_coefficient": 2,
    "tcc_total_cost": 748.98,
    "tcc_currency": "TRY",
    "tsp_enabled": true,
    "tsp_value": 1976.46,
    "tsp_formula": "TCC * 2.5 + 104",
    "tsp_currency": "TRY"
  }
}
💡 Use Case: Perfect for updating individual product information without fetching the entire catalog. Disable automatic hourly synchronizations and request only changed products on-demand.

POST /api/v1/sales

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

✨ Updated 2025-12-08: API v2.0 - Now uses only UUID (productId) format.

Request Body:

{
  "sales": [
    {
      "productId": "550e8400-e29b-41d4-a716-446655440000",  // Format: UUID only
      "date": "2025-11-03",            // Format: YYYY-MM-DD
      "quantity": 15,                  // Non-negative integer
      "stock_on_hand": 120             // Optional, non-negative integer
    },
    {
      "productId": "550e8400-e29b-41d4-a716-446655440000",  // UUID format required
      "date": "2025-11-03",
      "quantity": 8,
      "stock_on_hand": 45
    }
  ]
}

Field Formats:

FieldTypeRequiredFormat
productIdstring✅ YesUUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
datestring✅ YesYYYY-MM-DD
quantityinteger✅ Yes≥ 0
stock_on_handinteger❌ No≥ 0

Success Response:

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

Error Response:

{
  "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):

Login to PleasureStock → Products → Column "PleasureStock ID" shows the productId (UUID)

What Happens After:

  1. Data stored in PleasureStock
  2. AI calculates Smart Reordering recommendations (daily)
  3. View at: Cart → Smart Reordering tab
  4. See recommended quantities, stock levels, forecast accuracy

Best Practices:

  • 📅 Frequency: Send daily (minimum) or every 2-4 hours (optimal)
  • 📊 Historical data: Can submit 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:

{
  "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).

Out-of-Stock Days:

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

  • Exclude these days from average sales calculations
  • Mark them as OOS periods
  • Provide more accurate forecasts
{
  "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

Get AI-powered inventory recommendations based on sales history.

Query Parameters:

ParameterTypeRequiredDescription
datestringNoTarget date for predictions (default: today)

Response Example:

{
  "success": true,
  "date": "2024-03-20",
  "recommendations": [
    {
      "productId": "550e8400-e29b-41d4-a716-446655440000",
      "product_name": "Premium Widget",
      "product_sku": "WDG-001",
      "recommended_quantity": 25,
      "current_stock": 10,
      "average_daily_sales": 3.5,
      "forecast_accuracy": 0.87,
      "calculation_date": "2024-03-20T00:00:00Z"
    }
  ],
  "total": 15
}

GET /api/v1/currency/convert-to-try

Convert amounts to Turkish Lira using real-time exchange rates.

Query Parameters:

ParameterTypeRequiredDescription
amountnumberYesAmount to convert
currencystringYesSource currency (EUR, USD, GBP)

Response Example:

{
  "success": true,
  "originalAmount": 100,
  "originalCurrency": "EUR",
  "convertedAmount": 3487.50,
  "targetCurrency": "TRY",
  "exchangeRate": 34.875,
  "lastUpdated": "2024-03-20T12:00:00Z"
}

External B2B Integration API (v2 — current)

Purchasing-intelligence data exchange. You feed sales, stock, supply costs and delivery dates; you 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
Authentication: Authorization: Bearer YOUR_API_KEY (company-scoped; same key as Standard API)
Envelope: { "success": true, "data": {…} } or { "success": false, "error": "…", "details": [] }
Product key: our catalog productId (UUID) everywhere — map your SKUs first.
Idempotent & recalculating: re-sending the same keys updates the record and re-triggers calculations (no duplicates, no lost history).
Order identifier: match by delivery_id (PS-YYYY-NNNN) OR order_number (ORD-…).

Product mapping

GET /api/v2/external/catalog?limit=100&offset=0   # paginated catalog export → map your SKU → our productId (limit ≤ 500)
GET /api/v2/external/products/search?q=&sku=&limit=25  # spot lookup (name substring / SKU prefix)
GET /api/v2/external/matches                       # similarity-engine candidates when SKU mapping is impossible

# Optional ?include=description,tags,attributes on /catalog and /products/search — rich projection:
#   description = supplier-sourced product text (cleaned, English) — raw material for your own selling copy
#   tags        = OUR classification (controlled vocabulary from our AI consensus pipeline; not derivable from description)
#   attributes  = material (string), waterproof (STRING IP class e.g. "IPX5", NOT boolean), hasVibration/hasHeating (boolean),
#                 weightKg (string), dimensionsCm, unitsPerBox, productionTimeDays — any may be null
# Default stays lean (productId/sku/barcode/name/price/currency/supplier/category/categorySlug); unknown include values silently ignored.
# Each item: "category" = human-readable name (NOT a stable identifier — names are not unique). "categorySlug" = stable id → MATCH ON THIS.
# barcode = UPC/EAN/GTIN or null (strongest mapping key). price = string (DECIMAL). tags = display names, mixed casing → compare case-insensitively.
# tags may also contain legacy MATERIAL values (silicone/abs/tpe) — use the material attribute for material; ignore unknown tag values.
# Sample item (?include=description,tags,attributes): { "productId": "b147b193-…", "sku": "SHD-S392-2", "barcode": null,
#                "name": "Gino-RCT", "price": "12.35", "currency": "USD", "supplier": "S-Hande Technology Co.,Ltd",
#                "category": "Vibrators", "categorySlug": "vibrators", "description": "…",
#                "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 }
# All monetary/fractional DECIMAL fields (price, amount, averageDailySales, reorderPoint, daysUntilStockout…) are JSON strings — parse as decimal.
Categories & Tags — single axis. One category per product = "what is this thing" (~30 top-level categories, no sub-categories). Features = "what it's like" live in tags, grouped by form (Rabbit, Bullet, Wand, Suction…), style (Realistic, Fantasy…), zone (Clitoral, G-spot…), medium (Water-based), motion (Thrusting), wear (Wearable, Strapless…), segment (Pocket, Training kit, Accessories). A rabbit vibrator = category vibrators + tags Rabbit, Thrusting — NOT a "Rabbit" category. Former sub-categories (Rabbit, Realistic, Water-based, Pocket…) are now tags. Map on categorySlug.

PUSH — you send us data

POST /api/v2/external/sales      # Flow C — raw sales (append-only, idempotent on productId+date+marketplace+saleId)
{ "sales": [ { "productId": "uuid", "date": "2026-06-01", "units": 3, "price": 349.90, "currency": "TRY", "marketplace": "trendyol" } ] }

POST /api/v2/external/stock      # Flow C — point-in-time stock observations. Stock moves DURING the day (inventory/re-grading/receipts) — each is a real fact. Send observedAt; MANY per day allowed. We store ALL (movement history), never overwrite; "current stock" = latest observedAt. (Legacy "one/day, latest wins" retired 2026-07-01.)
# Optional out_of_stock (boolean) = direct availability flag; OVERRIDES the number for demand: on_hand>0 + out_of_stock:true (delisted) → day NOT counted; on_hand:0 + out_of_stock:false (re-grading, still selling) → day counted. Omit → fallback: on_hand:0 means OOS.
{ "stock": [ { "productId": "uuid", "observedAt": "2026-06-01T14:30:00Z", "on_hand": 134, "out_of_stock": false } ] }

POST /api/v2/external/orders/:ref/payments   # Flow B — cumulative payments → real landed cost; idempotent on (orderNumber,paymentId)
{ "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 ; :ref = delivery_id OR order_number

PUT /api/v1/external/orders/{orderNumber}/status   # Flow A — 7-state machine created→part_paid→paid→on_the_way→delivered→accepted (+cancelled). Served by v1 today; v2 :ref-based variant planned
{ "status": "on_the_way", "comment": "left warehouse" }

POST /api/v2/external/products   # Flow D — submit your listing → similarity matching

READ — you read our calculations

GET /api/v2/external/recommendations         # reorder recs per product; data.count = item count
{ "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 } } } ] } }
# DECIMAL fields (averageDailySales, forecastAccuracy, reorderPoint, safetyStock, daysUntilStockout) are strings.

GET /api/v2/external/demand/:productId?windowDays=30   # aggregated demand over the window
{ "data": { "productId": "uuid", "windowDays": 30, "daysWithSales": 0, "totalUnits": 0,
  "avgDailyUnits": 0, "weightedAvgPrice": null, "trend": "flat" } }   # trend: up | down | flat

GET /api/v2/external/orders/:ref/cost                  # landed cost / TCC (source: actual | estimated); payments[].amount = string
                                                        # + delivery ETA (shippingMethod, estimatedArrivalDate, estimatedArrivalBasis)
GET /api/v2/external/matches                           # similarity candidates for your listing
{ "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": "…", "sku": "SN22A", "price": "8.85", "currency": "USD",
      "supplier": "…", "score": 0.7041 } ] } ] } }

PULL feed — you expose, we poll hourly

Alternatively, publish a read-only token-protected feed; we poll hourly (configure base URL + token in your cabinet at /settings/feed). Sections: /orders, /payments, /sales, /stock, /products (all with from/to). /stock returns point-in-time observations with observedAt — may include several per product per day (stock movement); expose all, we keep all.

GET /orders?from&to   (your feed)
{ "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
} ] }
# Dates power the real lead time (acceptedDate − firstPaymentDate, per mode). Send real YYYY-MM-DD or omit — never a placeholder/now().

Marketplace commission is configured in your cabinet (per-marketplace), not sent in the feed.


Legacy — External B2B API v1

v1 (/api/v1/external) is legacy — still live for existing integrations. New clients: use v2 above.

Base URL: https://pleasurestock.com/api/v1/external
Authentication: Bearer token required
Rate Limit: 1,000 req/hour per API key; order creation 60 req/hour; RateLimit-* headers, 429 on excess

POST /api/v1/external/smart-reorder

Submit comprehensive sales data for AI-powered reordering recommendations.

Request Body:

{
  "products": [
    {
      "productId": "550e8400-e29b-41d4-a716-446655440001",      // UUID format
      "quantity": 15,                 // Non-negative number
      "current_stock": 85,            // Non-negative number
      "sales_history": [5, 8, 12, 6, 9, 11, 7]  // Min 7 days of data
    }
  ]
}

Response:

{
  "success": true,
  "data": {
    "smartReorderId": 123,
    "lastUpdated": "2024-03-20T15:30:00Z",
    "isActive": true
  }
}

Validation Rules:

  • quantity must be non-negative number
  • current_stock must be non-negative number
  • sales_history must contain at least 7 days of numeric data

POST /api/v1/external/orders

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

Request Body:

{
  "items": [
    {
      "productId": "550e8400-e29b-41d4-a716-446655440001",    // Product ID (UUID format)
      "quantity": 10,
      "customPrice": 25.50          // Optional: override product price
    },
    {
      "productId": "550e8400-e29b-41d4-a716-446655440001",    // Another product
      "quantity": 5
    }
  ],
  "externalOrderId": "ERP-2025-001",  // Optional: your reference ID
  "comment": "Urgent order"           // Optional: order comment
}

Response Example:

{
  "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.

GET /api/v1/external/orders

Retrieve orders for your company with optional filtering.

Query Parameters:

ParameterTypeRequiredDescription
statusstringNoFilter by status (created, part_paid, paid, on_the_way, delivered, accepted, cancelled)
limitintegerNoResults per page (default: 20)
offsetintegerNoSkip records (default: 0)

Response Example:

{
  "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 items[] line carries the catalog sku and an absolute public imageUrl (null if no photo) — enough to build product cards straight from the feed. Use statusDisplay for color-coded badges. 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}

Get detailed information about a specific order.

Response Example:

{
  "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": "WDG-001",
        "name": "Premium Widget",
        "imageUrl": "https://pleasurestock.com/uploads/products/wdg001.jpg",
        "quantity": 5,
        "unitPrice": 29.99,
        "totalPrice": 149.95,
        "currency": "EUR",
        "product": { "name": "Premium Widget", "sku": "WDG-001", "imageUrl": "https://pleasurestock.com/uploads/products/wdg001.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": "EUR"
      }
    ],
    "totalAmount": 1549.99,
    "currency": "EUR",
    "shippingMethod": "sea",
    "estimatedArrivalDate": "2024-06-28",
    "estimatedArrivalBasis": "first_payment",
    "estimatedDeliveryDate": "2024-06-28",
    "actualDeliveryDate": null,
    "createdAt": "2024-03-15T10:00:00Z",
    "confirmedAt": "2024-03-15T10:15:00Z",
    "paidAt": "2024-03-16T14:30:00Z",
    "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 is first_payment or shipped. It is null until the first payment arrives, and null after acceptance (use acceptedAt). estimatedDeliveryDate is a legacy alias of estimatedArrivalDate.
Catalog vs custom line items: switch on the isCustom boolean (present on every line in list, detail and the order.updated webhook — do not infer from productId === null alone).
isCustom: false → catalog product: productId (UUID), sku, name, imageUrl (absolute public URL).
isCustom: true → custom line (not in our catalog): productId: null; use customSku, customName, customCategory, customDescription; imageUrl: null.

PUT /api/v1/external/orders/{orderNumber}/status

Update order status in the workflow.

Request Body:

{
  "status": "paid",                     // Required: paid, on_the_way, accepted
  "externalOrderId": "EXT-123456",      // Optional: your reference
  "comment": "Payment confirmed"        // Optional: status comment
}
⚠️ 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.

Order Status Lifecycle:

Workflow: createdpart_paidpaidon_the_waydeliveredaccepted
Cancellation: Can be set from any non-final status → cancelled
Final statuses: accepted, cancelled
StatusSet ByDescriptionNotifications
createdSystemOrder created, awaiting paymentNone
part_paidExternal APIPartial payment receivedEmail to supplier
paidExternal APIFull payment confirmedEmail to supplier
on_the_wayExternal APIShipment dispatchedEmail to buyer
deliveredExternal APIGoods delivered to buyer (awaiting acceptance)Email to buyer and supplier
acceptedExternal APIGoods received and accepted by buyer (FINAL)Email to supplier
cancelledSystem/UserOrder 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 /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:

{
  "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. The cssClass uses Tailwind CSS classes.

Response:

{
  "success": true,
  "data": {
    "orderId": 1234,
    "orderNumber": "ORD-2024-001234",
    "status": "paid",
    "statusChangedAt": "2024-03-20T15:45:00Z"
  }
}

POST /api/v1/external/webhook

Endpoint for receiving webhook notifications about order events.

Webhook Events:

EventTriggerDescription
order.updatedAny order/delivery change (status, payment, shipping)Full order snapshot incl. items[] with sku + absolute imageUrl per line

PleasureStock emits one outbound event, order.updated, carrying the full current state of the order — so a single handler covers creation, payment, shipping and cancellation.

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:
{
  "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",
    "estimatedArrivalDate": "2025-03-22",
    "estimatedArrivalBasis": "shipped",
    "estimatedDeliveryDate": "2025-03-22",
    "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"
  }
}

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

All API endpoints return consistent error responses with appropriate HTTP status codes.

Error Response Format:

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

HTTP Status Codes:

CodeDescriptionCommon Causes
200SuccessRequest processed successfully
400Bad RequestInvalid parameters, validation errors
401UnauthorizedMissing or invalid API key
403ForbiddenInsufficient permissions (not a manager role)
404Not FoundResource doesn't exist
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer-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

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 Integration

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 createOrder(items, externalOrderId = null, comment = null) {
    try {
      const payload = { items };
      if (externalOrderId) payload.externalOrderId = externalOrderId;
      if (comment) payload.comment = comment;
      
      const response = await axios.post(
        `${this.baseURL}/external/orders`,
        payload,
        {
          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;
    }
  }

  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;
    }
  }

  async submitSmartReorder(products) {
    try {
      const response = await axios.post(
        `${this.baseURL}/external/smart-reorder`,
        { products },
        {
          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 examples
const api = new PleasureStockAPI('pk_live_your_api_key_here');

// Create new order
const items = [
  { productId: '550e8400-e29b-41d4-a716-446655440001', quantity: 10, customPrice: 25.50 },
  { productId: '550e8400-e29b-41d4-a716-446655440001', quantity: 5 }
];

api.createOrder(items, 'ERP-2025-001', 'Urgent order')
  .then(result => {
    console.log(`Created ${result.data.totalOrders} orders`);
    result.data.orders.forEach(order => {
      console.log(`  - ${order.orderNumber}: $${order.totalAmount}`);
    });
  });

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

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

// Submit smart reorder data
const products = [
  {
    productId: '550e8400-e29b-41d4-a716-446655440001',
    quantity: 15,
    current_stock: 85,
    sales_history: [5, 8, 12, 6, 9, 11, 7]
  }
];

api.submitSmartReorder(products)
  .then(result => {
    console.log('Smart reorder created:', result);
  });

Integration Best Practices

Security: Never expose your API key in client-side code or public repositories. Keep it secure on your server.
Rate Limiting: 1,000 requests/hour per API key (order creation: 60/hour). Standard RateLimit-* response headers are sent on every call — implement exponential backoff and respect them; exceeding a limit returns 429 with code RATE_LIMIT_EXCEEDED.
Order Creation: Items are automatically grouped by supplier. One API call may create multiple orders if items belong to different suppliers.
Webhook Processing: Always verify webhook signatures using HMAC-SHA256. Respond with 200 OK immediately and process asynchronously.
Status Updates: Only external API can set statuses part_paid, paid, on_the_way, and accepted. The system automatically sets created status.
Retry Mechanism: Implement retry logic with exponential backoff for failed requests. The system retries webhooks 3 times automatically.

Testing

Use the test script to verify your integration:

cd /root/b2b-supplier-system/backend
./test-external-api.sh

Replace YOUR_API_KEY_HERE in the script with your actual API key.

Support

  • Email: [email protected]
  • API Documentation: https://pleasurestock.com/api-docs
  • Interactive API Playground: https://pleasurestock.com/api-docs#playground
  • GitHub Issues: https://github.com/pleasurestock/api-integration