Complete reference for the SKUMan External API, enabling ERP, e-commerce, and other system integrations.
Quick Start
Section titled “Quick Start”1. Get an API Key
Section titled “1. Get an API Key”API keys are created by tenant administrators in the SKUMan Admin Dashboard.
2. Make Your First Request
Section titled “2. Make Your First Request”curl -X GET "https://your-instance.skuman.com/api/v1/external/products?limit=10" \ -H "X-API-Key: skm_live_your_key_here"3. Check the Response
Section titled “3. Check the Response”{ "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "sku": "NIKE-AIR-001", "name": "Air Max 90", "price": 129.99, "available": 250, "version": 5 } ], "pagination": { "limit": 10, "offset": 0, "total": 1250, "hasMore": true }}Authentication
Section titled “Authentication ”Include your API key in every request using one of these methods:
X-API-Key: skm_live_your_key_hereAuthorization: Bearer skm_live_your_key_hereAPI Key Format
Section titled “API Key Format”All API keys follow this format:
skm_live_[32 random characters]Example: skm_live_7Hk2mP9xQrL5nWdF3jYv8bAc1eGtKs6i
JWT Authentication (Management Endpoints)
Section titled “JWT Authentication (Management Endpoints)”API Key and Webhook management endpoints (/keys/*, /webhooks/*) require an Auth0 JWT instead of an API key. These are admin-only operations — the JWT must belong to a user with the Admin or SUPERAdmin role within the Auth0 Organization.
API Key vs JWT
Section titled “API Key vs JWT”| Auth method | Token format | Used for |
|---|---|---|
| API Key | skm_live_... (40 chars) | Product CRUD, image upload |
| JWT | eyJhbGciOi... (.-delimited, 3 segments) | API key management, webhook management |
Obtaining a token
Section titled “Obtaining a token”To obtain a token programmatically for testing:
- Configure an Auth0 SPA application with your redirect URI
- Initiate the authorization request with the correct audience:
https://{AUTH0_DOMAIN}/authorize? response_type=code& client_id={CLIENT_ID}& redirect_uri={REDIRECT_URI}& audience={AUTH0_AUDIENCE}& scope=openid email& organization={AUTH0_ORG_ID}& code_challenge={PKCE_CHALLENGE}& code_challenge_method=S256- Exchange the authorization code for tokens at
https://{AUTH0_DOMAIN}/oauth/token - Use the
access_token(not theid_token) in the Authorization header
Required JWT claims
Section titled “Required JWT claims”The JWT must contain these custom claims (injected by an Auth0 Post-Login Action):
| Claim | Description |
|---|---|
https://sku-man.com/org_id | Auth0 Organization ID (maps to tenant) |
https://sku-man.com/roles | Must include Admin or SUPERAdmin |
https://sku-man.com/email | User email (used for audit logging) |
Header format
Section titled “Header format”Authorization: Bearer eyJhbGciOiJSUzI1NiIs...JWT verification details
Section titled “JWT verification details”| Property | Value |
|---|---|
| Algorithm | RS256 |
| Issuer | https://{AUTH0_DOMAIN}/ |
| Audience | The API identifier configured in Auth0 |
Auth0 setup requirements
Section titled “Auth0 setup requirements”Permissions
Section titled “Permissions”| Permission | Access |
|---|---|
read | GET requests (list, retrieve products) |
write | POST, PUT, DELETE requests (create, update, delete, image upload) |
Key Rotation
Section titled “Key Rotation”Base URL
Section titled “Base URL ”https://{your-instance}.skuman.com/api/v1/externalEndpoints
Section titled “Endpoints ”Health
Section titled “Health”| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /health | None | Check API status |
Products
Section titled “Products”| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /products | API Key (read) | List products with filters |
| POST | /products | API Key (write) | Create product |
| GET | /products/{id} | API Key (read) | Get single product |
| PUT | /products/{id} | API Key (write) | Update product |
| DELETE | /products/{id} | API Key (write) | Soft-delete product |
| POST | /products/bulk | API Key (write) | Bulk upsert (up to 1,000) |
Image Upload
Section titled “Image Upload ”| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /products/{id}/images | API Key (write) | Upload image (multipart) |
| POST | /products/{id}/images/presign | API Key (write) | Get presigned upload URL |
| POST | /products/{id}/images/complete | API Key (write) | Complete presigned upload |
| POST | /staging/{stagingId}/images/presign | API Key (write) | Presign for staged product |
| POST | /staging/{stagingId}/images/complete | API Key (write) | Complete staged presigned upload |
API Keys
Section titled “API Keys ”| Method | Endpoint | Description |
|---|---|---|
| GET | /keys | List API keys |
| POST | /keys | Create API key |
| GET | /keys/{id} | Get API key details |
| PATCH | /keys/{id} | Update API key |
| DELETE | /keys/{id} | Revoke API key |
| POST | /keys/{id}/rotate | Rotate API key |
Webhooks
Section titled “Webhooks ”| Method | Endpoint | Description |
|---|---|---|
| GET | /webhooks | List webhooks |
| POST | /webhooks | Create webhook |
| GET | /webhooks/{id} | Get webhook details |
| PATCH | /webhooks/{id} | Update webhook |
| DELETE | /webhooks/{id} | Delete webhook |
| POST | /webhooks/{id}/rotate-secret | Rotate webhook signing secret |
| POST | /webhooks/{id}/test | Send test event |
| GET | /webhooks/{id}/deliveries | View delivery log |
Rate Limiting
Section titled “Rate Limiting ”Per-Key Limits
Section titled “Per-Key Limits”| Limit | Default | Configurable Range |
|---|---|---|
| Per minute | 60 requests | 1 - 10,000 |
| Per day | 10,000 requests | 1 - 1,000,000 |
Per-Tenant Aggregate Limit
Section titled “Per-Tenant Aggregate Limit”All API keys for a single tenant share a combined limit of 600 requests/minute.
Response Headers
Section titled “Response Headers”| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per minute for this key |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Unix timestamp (ms) when window resets |
Retry-After | Seconds to wait (only on 429 responses) |
Tracing
Section titled “Tracing ”Every response includes an X-Trace-Id header for debugging. You can send your own X-Trace-Id request header and the API will use it instead of generating one.
The trace ID appears in all error responses in the traceId field.
Error Handling
Section titled “Error Handling ”Error Response Format
Section titled “Error Response Format”{ "error": { "code": "ERROR_CODE", "message": "Human-readable description", "traceId": "abc123def456", "details": {} }}Error Codes
Section titled “Error Codes”| Code | Status | Description |
|---|---|---|
VALIDATION_ERROR | 400 | Invalid request body or parameters |
INVALID_JSON | 400 | Malformed JSON body |
MISSING_REQUIRED_FIELD | 400 | Required field absent |
UNAUTHORIZED | 401 | No API key provided |
INVALID_API_KEY | 401 | API key not found or bad format |
EXPIRED_API_KEY | 401 | API key has expired |
REVOKED_API_KEY | 401 | API key has been revoked |
FORBIDDEN | 403 | Access denied (e.g., upload session mismatch) |
INSUFFICIENT_PERMISSIONS | 403 | API key lacks required permission |
NOT_FOUND | 404 | Resource not found |
PRODUCT_NOT_FOUND | 404 | Product does not exist |
WEBHOOK_NOT_FOUND | 404 | Webhook not found |
API_KEY_NOT_FOUND | 404 | API key not found |
VERSION_CONFLICT | 409 | Optimistic locking conflict |
DUPLICATE_SKU | 409 | SKU already exists |
DUPLICATE_KEY | 409 | Unique constraint violation |
UNPROCESSABLE_ENTITY | 422 | Semantic validation failure |
RATE_LIMIT_EXCEEDED | 429 | Rate limit exceeded |
INTERNAL_ERROR | 500 | Unexpected server error |
SERVER_ERROR | 500 | Server processing error |
DATABASE_ERROR | 500 | Database error |
SERVICE_UNAVAILABLE | 503 | Service temporarily unavailable |
Idempotency
Section titled “Idempotency ”For safe retries on write operations (POST, PUT, PATCH, DELETE), include an idempotency key:
Idempotency-Key: unique-operation-idAPI Import Staging
Section titled “API Import Staging ”Staged Response Format
Section titled “Staged Response Format”{ "staged": true, "batchId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "stagingId": "550e8400-e29b-41d4-a716-446655440000", "operation": "create", "sku": "NIKE-AIR-001", "message": "Product staged for admin review"}What is NOT staged
Section titled “What is NOT staged”- Image uploads always execute immediately (FK constraint requires the product to exist)
- GET requests are never affected by staging
Bulk staging response
Section titled “Bulk staging response”{ "staged": true, "batchId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "summary": { "total": 50, "creates": 10, "updates": 40, "inputRows": 50 }, "message": "50 products staged for admin review"}Products API
Section titled “Products API ”Health Check
Section titled “Health Check ”/health No authentication required.
{ "status": "ok", "version": "1.0", "timestamp": 1704067200000}List Products
Section titled “List Products ”/products Retrieve products with filtering and pagination.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 100 | Results per page (1-1000) |
offset | integer | 0 | Number of results to skip |
updatedSince | integer | Unix timestamp (ms) - only products updated after this time | |
includeDeleted | boolean | false | Include soft-deleted products (tombstones) |
brand | string | Filter by exact brand match | |
season | string | Filter by exact season match | |
division | string | Filter by exact division match | |
bundle | string | Filter by exact bundle match | |
minPrice | number | Minimum price filter | |
maxPrice | number | Maximum price filter | |
tags | string | Comma-separated tag names (matches product_tags OR webitem_tags) | |
search | string | Case-insensitive search on SKU, name, description (max 200 chars) |
Example Request
Section titled “Example Request”curl "https://api.skuman.com/api/v1/external/products?limit=50&brand=Nike&tags=bestseller,new-arrival&updatedSince=1704067200000" \ -H "X-API-Key: skm_live_..."Example Response
Section titled “Example Response”{ "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "sku": "NIKE-AIR-001", "name": "Air Max 90", "brand": "Nike", "season": "SS24", "price": 129.99, "retailPrice": 150.00, "available": 250, "images": ["https://cdn.example.com/image1.jpg"], "tags": ["bestseller", "new-arrival"], "updatedAt": 1704067200000, "version": 5 } ], "pagination": { "limit": 50, "offset": 0, "total": 1250, "hasMore": true }}Incremental Sync Pattern
Section titled “Incremental Sync Pattern”# Initial full syncGET /products?limit=1000
# Subsequent incremental syncsGET /products?updatedSince=1704067200000&includeDeleted=trueGet Product
Section titled “Get Product ”/products/{id} Retrieve a single product by UUID.
Response (200 OK)
Section titled “Response (200 OK)”{ "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "sku": "NIKE-AIR-001", "name": "Air Max 90", "brand": "Nike", "price": 129.99, "version": 5, "updatedAt": 1704067200000 }}Create Product
Section titled “Create Product ”/products Create a new product. Requires write permission.
Headers
Section titled “Headers”| Header | Required | Description |
|---|---|---|
X-API-Key | Yes | Your API key |
Content-Type | Yes | application/json |
Idempotency-Key | Recommended | Unique key for safe retries |
Request Body
Section titled “Request Body”{ "sku": "NIKE-AIR-001", "name": "Air Max 90", "brand": "Nike", "season": "SS24", "price": 129.99, "retailPrice": 150.00, "available": 250, "tags": ["new-arrival"]}Response (201 Created)
Section titled “Response (201 Created)”{ "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "sku": "NIKE-AIR-001", "name": "Air Max 90", "brand": "Nike", "version": 1, "createdAt": 1704067200000, "updatedAt": 1704067200000 }}Update Product
Section titled “Update Product ”/products/{id} Update an existing product. Requires write permission.
Optimistic Locking
Section titled “Optimistic Locking”Request Body
Section titled “Request Body”{ "price": 139.99, "available": 200, "version": 5}Response (200 OK)
Section titled “Response (200 OK)”{ "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "sku": "NIKE-AIR-001", "price": 139.99, "available": 200, "version": 6, "updatedAt": 1704070800000 }}Version Conflict Response (409)
Section titled “Version Conflict Response (409)”{ "error": { "code": "VERSION_CONFLICT", "message": "Version conflict", "traceId": "abc123", "details": { "currentVersion": 6, "providedVersion": 5 } }}Delete Product
Section titled “Delete Product ”/products/{id} Soft-delete a product. Requires write permission. The product becomes a tombstone visible via includeDeleted=true.
Response (200 OK)
Section titled “Response (200 OK)”{ "success": true, "message": "Product deleted"}Bulk Upsert
Section titled “Bulk Upsert ”/products/bulk Create or update up to 1,000 products in a single request. Requires write permission.
Request Body
Section titled “Request Body”| Field | Type | Default | Description |
|---|---|---|---|
products | array | Array of products (1-1000) | |
matchBy | string | id | Match by id or sku |
createIfMissing | boolean | true | Create products that don't exist |
Example Request
Section titled “Example Request”{ "products": [ { "sku": "SKU-001", "name": "Product 1", "price": 99.99, "available": 100 }, { "sku": "SKU-002", "name": "Product 2", "price": 149.99, "available": 50 }, { "sku": "SKU-003", "name": "Product 3", "price": 199.99, "available": 25 } ], "matchBy": "sku", "createIfMissing": true}Success Response
Section titled “Success Response”{ "success": true, "summary": { "total": 3, "created": 1, "updated": 2, "failed": 0 }, "errors": []}Partial Failure Response
Section titled “Partial Failure Response”{ "success": false, "summary": { "total": 3, "created": 1, "updated": 1, "failed": 1 }, "errors": [ { "sku": "SKU-003", "error": "Invalid price format" } ]}Image Upload API
Section titled “Image Upload API ”Images can be uploaded to products via the External API. Two methods are supported.
Multipart Upload
Section titled “Multipart Upload ”/products/{id}/images Upload a single image file directly.
curl -X POST "https://api.skuman.com/api/v1/external/products/550e8400-.../images" \ -H "X-API-Key: skm_live_..." \ -F "image=@product-photo.jpg"Response (201 Created)
Section titled “Response (201 Created)”{ "url": "https://cdn.example.com/tenant-id/1704067200000-abc-product-photo.jpg", "deduplicated": false, "staged": false}Ambiguous Conflict (202)
Section titled “Ambiguous Conflict (202)”If an image with the same normalized filename but different content already exists:
{ "staged": true, "conflict": true, "batchId": "f47ac10b-...", "stagingId": "550e8400-...", "deduplicated": false, "message": "Image upload staged for manual conflict resolution"}Presigned URL Upload
Section titled “Presigned URL Upload ”For large files or browser-based uploads. This is a two-step process.
Step 1: Request Presigned URL
Section titled “Step 1: Request Presigned URL”/products/{id}/images/presign { "fileName": "product-photo.jpg", "mimeType": "image/jpeg", "fileHash": "a1b2c3d4e5f6...", "fileSize": 102400}| Field | Type | Required | Description |
|---|---|---|---|
fileName | string | Yes | Image filename (max 255 chars, no path separators) |
mimeType | string | Yes | image/jpeg, image/png, image/webp, or image/gif |
fileHash | string | No | SHA-256 hex hash (64 chars) for dedup hint |
fileSize | integer | No | File size in bytes |
Response (201):
{ "uploadSessionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "uploadUrl": "https://r2-presigned-url...", "key": "tenant-id/1704067200000-abc-product-photo.jpg", "expiresInSeconds": 900}Step 2: Upload File
Section titled “Step 2: Upload File”Upload directly to the uploadUrl using HTTP PUT:
curl -X PUT "https://r2-presigned-url..." \ -H "Content-Type: image/jpeg" \ --data-binary @product-photo.jpgStep 3: Complete Upload
Section titled “Step 3: Complete Upload”/products/{id}/images/complete { "uploadSessionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479"}Response (201):
{ "url": "https://cdn.example.com/tenant-id/1704067200000-abc-product-photo.jpg", "deduplicated": false, "staged": false}Image Processing
Section titled “Image Processing ”| Aspect | Behavior |
|---|---|
| Original image | Stored as-is — no trimming, resizing, or re-encoding |
| Thumbnails | Generated in background at 100px, 400px, 600px (JPEG) |
| Dedup skip | Thumbnails skipped if image was deduplicated (already exist) |
| URL pattern | {CDN_URL}/{tenant-id}/thumbnails/{filename}_w{width}.jpg |
| Availability | Asynchronous — may not be immediately available after upload |
Presigned Upload for Staged Products
Section titled “Presigned Upload for Staged Products ”When API Import Staging is enabled, newly created products don't exist in the database yet. Use staging-specific endpoints:
POST /staging/{stagingId}/images/presignPOST /staging/{stagingId}/images/completeThese work identically to the product presigned endpoints but link the image to a staged item. The image is committed when the batch is committed.
Webhooks
Section titled “Webhooks ”Real-time notifications for product changes. Webhooks fire for both External API mutations and internal UI changes.
Events
Section titled “Events”| Event | Trigger |
|---|---|
product.created | New product created |
product.updated | Product modified (including image changes) |
product.deleted | Product soft-deleted |
product.bulk_updated | Bulk operation completed (one per sub-batch of 200) |
inventory.updated | Incoming inventory upserted or bulk imported |
inventory.deleted | Single incoming inventory entry deleted |
inventory.cleared | All incoming inventory cleared (admin) |
Webhook Headers
Section titled “Webhook Headers”Every webhook request includes these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | SKUMan-Webhooks/1.0 |
X-SKUMan-Signature | HMAC signature: v1={hmac_sha256_hex} |
X-SKUMan-Old-Signature | Old HMAC signature (only during 24h secret rotation grace period) |
X-SKUMan-Timestamp | Unix timestamp (seconds) when signed |
X-SKUMan-Event | Event type (e.g., product.updated) |
X-SKUMan-Delivery-ID | Unique delivery UUID |
Payload Format
Section titled “Payload Format”{ "id": "550e8400-e29b-41d4-a716-446655440000", "sku": "NIKE-AIR-001", "product": { "id": "550e8400-...", "sku": "NIKE-AIR-001", "name": "Air Max 90", "price": 139.99, "updatedAt": 1704067200000, "version": 6 }, "changes": ["price"]}Bulk Updated Payload
Section titled “Bulk Updated Payload”The product.bulk_updated event fires once per sub-batch (200 products). The payload shape varies by source.
External API (source: "external_api")
Section titled “External API (source: "external_api")”{ "batchStart": 0, "batchSize": 200, "created": 5, "updated": 195, "createdProducts": [{ "id": "550e8400-...", "sku": "SKU-001" }], "updatedProducts": [{ "id": "6ba7b810-...", "sku": "SKU-002" }], "source": "external_api", "batchId": null}Internal UI (source: "internal")
Section titled “Internal UI (source: "internal")”{ "total": 200, "created": 5, "updated": 195, "createdProducts": [{ "id": "550e8400-...", "sku": "SKU-001" }], "updatedProducts": [{ "id": "6ba7b810-...", "sku": "SKU-002" }], "source": "internal"}| Field | Type | Present | Description |
|---|---|---|---|
batchStart | integer | External API only | Starting index of this sub-batch |
batchSize | integer | External API only | Number of products in this sub-batch |
total | integer | Internal only | Total products in the save operation |
created | integer | Always | Number of products created |
updated | integer | Always | Number of products updated |
createdProducts | {id, sku}[] | Always | Products that were created |
updatedProducts | {id, sku}[] | Always | Products that were updated |
source | string | Always | "external_api" or "internal" |
batchId | string | null | External API only | Idempotency batch ID |
Test Webhook Payload
Section titled “Test Webhook Payload”The POST /webhooks/{id}/test endpoint sends a test event with event type "test":
{ "test": true, "timestamp": 1704067200000}Inventory Event Payloads
Section titled “Inventory Event Payloads”Inventory events have different payload shapes from product events.
inventory.updated
Section titled “inventory.updated”Fires on upsert or bulk import.
{ "action": "upsert", "entryCount": 5, "inserted": 3, "updated": 2, "productIds": ["550e8400-e29b-41d4-a716-446655440000", "6ba7b810-9dad-11d1-80b4-00c04fd430c8"]}| Field | Type | Description |
|---|---|---|
action | string | "upsert" for single upsert, "import" for bulk import |
entryCount | integer | Total entries in the request |
inserted | integer | New entries created |
updated | integer | Existing entries updated |
productIds | string[] | Distinct product UUIDs affected |
inventory.deleted
Section titled “inventory.deleted”Fires when a single entry is deleted.
{ "entryId": "550e8400-e29b-41d4-a716-446655440000", "deleted": 1}| Field | Type | Description |
|---|---|---|
entryId | string | UUID of the deleted inventory entry |
deleted | integer | Number of rows deleted (typically 1) |
inventory.cleared
Section titled “inventory.cleared”Fires when an admin clears all incoming inventory.
{ "deletedEntries": 150, "updatedProducts": 42}| Field | Type | Description |
|---|---|---|
deletedEntries | integer | Total inventory entries removed |
updatedProducts | integer | Products whose inventory fields were reset |
Signature Verification
Section titled “Signature Verification”Signature format: v1={hmac_sha256_hex}
const crypto = require('crypto');
function verifyWebhookSignature(req, secret) { const timestamp = req.headers['x-skuman-timestamp']; const signature = req.headers['x-skuman-signature']; const body = JSON.stringify(req.body);
// Verify timestamp is recent (within 5 minutes) const now = Math.floor(Date.now() / 1000); if (Math.abs(now - parseInt(timestamp)) > 300) { return false; }
// Calculate expected signature const payload = `${timestamp}.${body}`; const expected = 'v1=' + crypto .createHmac('sha256', secret) .update(payload) .digest('hex');
return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) );}import hmacimport hashlibimport time
def verify_webhook_signature(timestamp, body, signature, secret): # Verify timestamp is recent now = int(time.time()) if abs(now - int(timestamp)) > 300: return False
# Calculate expected signature payload = f"{timestamp}.{body}" expected = "v1=" + hmac.new( secret.encode(), payload.encode(), hashlib.sha256 ).hexdigest()
return hmac.compare_digest(signature, expected)Secret Rotation
Section titled “Secret Rotation”During the 24-hour grace period after rotating a webhook secret, both X-SKUMan-Signature (new secret) and X-SKUMan-Old-Signature (old secret) are sent. Verify against both during the transition.
Retry Policy
Section titled “Retry Policy”Failed deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
| 6 | 2 hours |
Endpoint Requirements
Section titled “Endpoint Requirements”Your webhook endpoint must:
- Respond within 30 seconds
- Return a 2xx status code for success
- Use HTTPS (except localhost for development)
- Not resolve to a private IP address (SSRF protection)
API Key Management
Section titled “API Key Management ”Create Key
Section titled “Create Key ”/keys { "name": "E-commerce Sync", "permissions": { "read": true, "write": false }, "rateLimitPerMinute": 120, "rateLimitPerDay": 50000, "expiresAt": "2026-12-31T00:00:00Z"}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | Key name (1-100 chars) | |
permissions.read | boolean | No | true | Allow read access |
permissions.write | boolean | No | false | Allow write access |
rateLimitPerMinute | integer | No | 60 | Per-minute rate limit (1-100,000) |
rateLimitPerDay | integer | No | 10,000 | Per-day rate limit (1-100,000,000) |
expiresAt | string | No | ISO 8601 expiration date |
Rotate Key
Section titled “Rotate Key ”/keys/{id}/rotate Creates a new key with the same settings. The old key remains valid for 24 hours.
{ "data": { "id": "new-key-uuid", "key": "skm_live_...", "oldKeyId": "old-key-uuid", "gracePeriodEnds": "2026-01-02T00:00:00.000Z" }, "message": "API key rotated. Old key will remain valid for 24 hours."}Webhook Management
Section titled “Webhook Management ”Create Webhook
Section titled “Create Webhook ”/webhooks { "name": "E-commerce Webhook", "url": "https://shop.example.com/api/webhooks/inventory", "events": ["product.created", "product.updated", "product.deleted"], "secret": "my-custom-secret-at-least-16-chars"}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | Webhook name (1-100 chars) | |
url | string | Yes | Endpoint URL (HTTPS required, except localhost) | |
events | string[] | No | ["product.created", "product.updated"] | Events to subscribe to |
secret | string | No | Auto-generated whsec_... | Signing secret (16-64 chars) |
Rotate Webhook Secret
Section titled “Rotate Webhook Secret ”/webhooks/{id}/rotate-secret Generates a new signing secret. The old secret remains valid for 24 hours. During the grace period, both X-SKUMan-Signature and X-SKUMan-Old-Signature headers are sent.
{ "data": { "id": "550e8400-...", "name": "E-commerce Webhook", "url": "https://shop.example.com/...", "events": ["product.created", "product.updated"], "is_active": true, "secret": "whsec_newSecret123...", "gracePeriodEnds": "2026-01-02T00:00:00.000Z" }, "message": "Secret rotated. Old secret valid for 24 hours. Store the new secret securely."}Delivery Log
Section titled “Delivery Log ”/webhooks/{id}/deliveries | Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Results per page (1-100) |
offset | integer | 0 | Number to skip |
status | string | Filter: pending, success, failed, retrying |
{ "data": [ { "id": "delivery-uuid", "event_type": "product.updated", "status": "success", "attempt_number": 1, "attempted_at": "2026-01-15T12:00:00Z", "response_status": 200, "response_time_ms": 150, "error_message": null, "next_retry_at": null } ]}Product Schema
Section titled “Product Schema ”| Field | Type | Max Length | Description |
|---|---|---|---|
id | uuid | Unique product identifier (auto-generated) | |
sku | string | 50 | Required. Unique product SKU |
name | string | 255 | Product name |
masterStyleNumber | string | 50 | Style number |
brand | string | 100 | Brand name |
season | string | 50 | Season code (e.g., "SS24") |
description | string | 5000 | Product description |
tabName | string | 100 | Tab/category name |
group | string | 100 | Group |
className | string | 100 | Class |
price | number | Wholesale price (>= 0) | |
retailPrice | number | Suggested retail price (>= 0) | |
currency | string | 10 | Currency code (default: USD) |
available | integer | Available quantity (>= 0) | |
totalOnOrder | integer | Quantity on order (>= 0) | |
ats | integer | Available to sell (>= 0) | |
size | string | 50 | Size |
colorCode | string | 50 | Color code |
colorGroup | string | 100 | Color group/family |
body | string | 100 | Body/style |
bundle | string | 100 | Bundle/collection |
division | string | 50 | Division |
webitemid | string | 100 | Variant group ID (groups color/size variants) |
webitemname | string | 255 | Variant group name |
images | string[] | Array of image URLs | |
tags | string[] | 50 each | Array of tag names |
assignedBuyerIds | string[] | Assigned buyer IDs | |
incomingInventory | array | { date: string, quantity: integer } entries | |
lockedFields | string[] | Fields locked from editing | |
udf1-udf8 | mixed | User-defined fields | |
version | integer | Version for optimistic locking (>= 1) | |
createdAt | integer | Creation timestamp (ms, read-only) | |
updatedAt | integer | Last update timestamp (ms, read-only) | |
_deleted | boolean | Tombstone marker (read-only) | |
deletedAt | integer | Deletion timestamp (ms, read-only) |
Code Examples
Section titled “Code Examples ”const SKUMAN_API = 'https://api.skuman.com/api/v1/external';const API_KEY = process.env.SKUMAN_API_KEY;
// List productsasync function listProducts(options = {}) { const params = new URLSearchParams(options); const response = await fetch(`${SKUMAN_API}/products?${params}`, { headers: { 'X-API-Key': API_KEY } }); return response.json();}
// Create productasync function createProduct(product) { const response = await fetch(`${SKUMAN_API}/products`, { method: 'POST', headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json', 'Idempotency-Key': `create-${product.sku}` }, body: JSON.stringify(product) }); return response.json();}
// Bulk upsertasync function bulkUpsert(products) { const response = await fetch(`${SKUMAN_API}/products/bulk`, { method: 'POST', headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json', 'Idempotency-Key': `bulk-${Date.now()}` }, body: JSON.stringify({ products, matchBy: 'sku', createIfMissing: true }) }); return response.json();}
// Upload imageasync function uploadImage(productId, filePath) { const formData = new FormData(); formData.append('image', new Blob([fs.readFileSync(filePath)]), 'photo.jpg'); const response = await fetch(`${SKUMAN_API}/products/${productId}/images`, { method: 'POST', headers: { 'X-API-Key': API_KEY }, body: formData }); return response.json();}
// Incremental syncasync function syncProducts(lastSyncTime) { let offset = 0; const limit = 500; let hasMore = true;
while (hasMore) { const { data, pagination } = await listProducts({ updatedSince: lastSyncTime, includeDeleted: true, limit, offset });
for (const product of data) { if (product._deleted) { await deleteLocalProduct(product.id); } else { await upsertLocalProduct(product); } }
hasMore = pagination.hasMore; offset += limit; }
return Date.now();}import requestsimport osimport time
SKUMAN_API = 'https://api.skuman.com/api/v1/external'API_KEY = os.environ['SKUMAN_API_KEY']
headers = { 'X-API-Key': API_KEY, 'Content-Type': 'application/json'}
# List productsdef list_products(**kwargs): response = requests.get( f'{SKUMAN_API}/products', headers=headers, params=kwargs ) return response.json()
# Create productdef create_product(product): response = requests.post( f'{SKUMAN_API}/products', headers={**headers, 'Idempotency-Key': f"create-{product['sku']}"}, json=product ) return response.json()
# Bulk upsertdef bulk_upsert(products): response = requests.post( f'{SKUMAN_API}/products/bulk', headers=headers, json={ 'products': products, 'matchBy': 'sku', 'createIfMissing': True } ) return response.json()
# Upload imagedef upload_image(product_id, file_path): with open(file_path, 'rb') as f: return requests.post( f'{SKUMAN_API}/products/{product_id}/images', headers={'X-API-Key': API_KEY}, files={'image': f} ).json()
# Incremental syncdef sync_products(last_sync_time): offset = 0 limit = 500 has_more = True
while has_more: result = list_products( updatedSince=last_sync_time, includeDeleted=True, limit=limit, offset=offset )
for product in result['data']: if product.get('_deleted'): delete_local_product(product['id']) else: upsert_local_product(product)
has_more = result['pagination']['hasMore'] offset += limit
return int(time.time() * 1000)# Health checkcurl "https://api.skuman.com/api/v1/external/health"
# List productscurl "https://api.skuman.com/api/v1/external/products?limit=10" \ -H "X-API-Key: skm_live_..."
# List products with tag filtercurl "https://api.skuman.com/api/v1/external/products?tags=bestseller,new-arrival" \ -H "X-API-Key: skm_live_..."
# Get single productcurl "https://api.skuman.com/api/v1/external/products/550e8400-..." \ -H "X-API-Key: skm_live_..."
# Create productcurl -X POST "https://api.skuman.com/api/v1/external/products" \ -H "X-API-Key: skm_live_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: create-ABC123" \ -d '{"sku":"ABC-123","name":"Test Product","price":99.99}'
# Update product with versioncurl -X PUT "https://api.skuman.com/api/v1/external/products/550e8400-..." \ -H "X-API-Key: skm_live_..." \ -H "Content-Type: application/json" \ -d '{"price":109.99,"version":5}'
# Delete productcurl -X DELETE "https://api.skuman.com/api/v1/external/products/550e8400-..." \ -H "X-API-Key: skm_live_..."
# Upload imagecurl -X POST "https://api.skuman.com/api/v1/external/products/550e8400-.../images" \ -H "X-API-Key: skm_live_..." \ -F "image=@product-photo.jpg"
# Bulk upsertcurl -X POST "https://api.skuman.com/api/v1/external/products/bulk" \ -H "X-API-Key: skm_live_..." \ -H "Content-Type: application/json" \ -d '{ "products": [ {"sku":"SKU-001","name":"Product 1","price":99.99}, {"sku":"SKU-002","name":"Product 2","price":149.99} ], "matchBy": "sku", "createIfMissing": true }'Best Practices
Section titled “Best Practices ”1. Use Idempotency Keys
Always include idempotency keys for write operations to handle network retries safely.
headers: { 'Idempotency-Key': `create-${product.sku}-${Date.now()}`}2. Implement Incremental Sync
Don't fetch all products every time. Use updatedSince with includeDeleted=true for efficient syncing.
const { data } = await listProducts({ updatedSince: lastSyncTime, includeDeleted: true});3. Handle Version Conflicts
Implement retry logic for version conflicts - fetch fresh data and retry.
async function updateWithRetry(id, updates, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { const current = await getProduct(id); try { return await updateProduct(id, { ...updates, version: current.data.version }); } catch (e) { if (e.code !== 'VERSION_CONFLICT') throw e; } } throw new Error('Max retries exceeded');}4. Respect Rate Limits
Monitor X-RateLimit-Remaining and implement backoff when approaching limits.
if (response.headers.get('X-RateLimit-Remaining') < 10) { await sleep(1000); // Slow down}5. Use Bulk Operations
For large updates, use bulk upsert (up to 1,000 products) instead of individual requests.
// Bad: 1000 individual requestsfor (const p of products) await createProduct(p);
// Good: 1 bulk requestawait bulkUpsert(products);6. Secure Your API Keys
- Never commit API keys to version control
- Use environment variables or secret managers
- Rotate keys regularly (at least quarterly)
- Use read-only keys when write access isn't needed
7. Verify Webhook Signatures
Always verify HMAC signatures and check timestamp freshness to prevent replay attacks. During secret rotation, check both X-SKUMan-Signature and X-SKUMan-Old-Signature.
8. Handle Staging Responses
If the tenant has API Import Staging enabled, write operations return 202 instead of the usual success codes. Your integration should handle both 201/200 (live) and 202 (staged) responses gracefully.
Last updated: February 2026