Skip to main content

Service Manager

Complete service management including categories, services, bundles, and archive functionality.

This demo is self-contained - it creates two merchants during setup:

  • Empty State Merchant - Shows the empty state UI (no categories/services)
  • Steady State Merchant - Pre-seeded with categories, services, and a bundle

Switch between merchants using the "View as" toggle to explore both states.


ResourceDescription
Service Categories APICreate, list, update, delete categories
Services APIFull service CRUD with variants
Service Packages APIBundle/package management

Interactive Demo

Loading...

Workflows

1. Empty State → First Category

When a merchant has no categories, the UI shows "No Categories Available".

import { ServiceCategoryService } from '@scp/sdk';

// Create first category
const category = await ServiceCategoryService.serviceCategoryCreate({
requestBody: {
name: 'Hair & Styling',
description: 'Haircuts, coloring, styling, and extensions',
tenant_id: tenantId,
merchant_id: merchantId
}
});

console.log('Created category:', category.id);

2. Create Service Under Category

import { ServiceService } from '@scp/sdk';

// Step 1: Create the service
const service = await ServiceService.serviceCreate({
requestBody: {
name: 'Hair Color',
description: 'Full hair coloring service',
default_duration_minutes: 90,
default_price: { amount: 12500, currency: 'USD' }, // $125.00
ui_parameters: { calendar_color: 'pink' },
is_active: true,
tenant_id: tenantId,
merchant_id: merchantId
}
});

// Step 2: Assign to category
await ServiceService.serviceAddCategory({
serviceId: service.id,
requestBody: { category_id: categoryId }
});

3. Create Service Bundle

Bundles combine multiple services into a package.

import { ServicePackageService } from '@scp/sdk';

// Create a bundle with discount pricing
const bundle = await ServicePackageService.servicePackageCreate({
requestBody: {
name: 'Platinum Hair Package',
description: 'Includes cut, color, and styling',
service_type: 'bundle',
default_price: { amount: 20000, currency: 'USD' }, // $200 (vs $250 separate)
is_active: true,
tenant_id: tenantId,
merchant_id: merchantId
}
});

console.log('Bundle created:', bundle.id);

4. Archive/Restore Services

Archive hides services from booking without deleting them.

// Archive a service (set is_active to false)
await ServiceService.serviceUpdate({
id: serviceId,
requestBody: {
is_active: false,
tenant_id: tenantId,
merchant_id: merchantId
}
});

// Restore a service
await ServiceService.serviceUpdate({
id: serviceId,
requestBody: {
is_active: true,
tenant_id: tenantId,
merchant_id: merchantId
}
});

5. Create Service Variant

Variants are alternative versions of a service (e.g., different durations).

// Create a variant of an existing service
const variant = await ServiceService.serviceCreateVariant({
serviceId: parentServiceId,
requestBody: {
name: 'Hair Color - Express',
description: 'Quick touch-up coloring',
default_duration_minutes: 45,
default_price: { amount: 7500, currency: 'USD' }, // $75.00
tenant_id: tenantId,
merchant_id: merchantId
}
});

// Variants are automatically linked to parent via parent_service_id
console.log('Variant parent:', variant.parent_service_id);

API Reference

Service Categories

import { ServiceCategoryService } from '@scp/sdk';

const result = await ServiceCategoryService.serviceCategoryList({
page: 1,
pageSize: 50,
orderBy: 'name'
});

// Response
{
data: [
{
id: 'uuid',
name: 'Hair & Styling',
description: 'Haircuts, coloring, styling',
order_index: 0,
is_active: true,
merchant_id: 'uuid',
tenant_id: 'uuid',
inserted_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z'
}
],
meta: {
total_count: 5,
page: 1,
page_size: 50
}
}

Services

import { ServiceService } from '@scp/sdk';

// List only base services (excludes variants and bundles)
const result = await ServiceService.serviceList({
page: 1,
pageSize: 50
});

// Each service includes:
// - id, name, description
// - default_duration_minutes
// - default_price: { amount, currency }
// - ui_parameters: { calendar_color }
// - service_type: 'standalone' | 'variant' | 'bundle'
// - is_active
// - categories: [{ id, name }]

Service-Category Mappings

// Get categories assigned to a service
const result = await ServiceService.serviceListCategories({
serviceId: serviceId
});

// Returns array of categories
console.log(result.data);

Service Variants

// Get all variants of a service
const result = await ServiceService.serviceListVariants({
serviceId: parentServiceId
});

// Variants have parent_service_id set
result.data.forEach(v => {
console.log(`${v.name} (parent: ${v.parent_service_id})`);
});

Service Packages (Bundles)

import { ServicePackageService } from '@scp/sdk';

// List only bundles (service_type: 'bundle')
const result = await ServicePackageService.servicePackageList({
page: 1,
pageSize: 50
});

// Each bundle includes bundled services
result.data.forEach(bundle => {
console.log(`${bundle.name}: ${bundle.services?.length} services`);
});

UI Data Models

Service Object

interface Service {
id: string;
name: string;
description?: string;
default_duration_minutes?: number;
default_price?: {
amount: number; // In cents
currency: string; // 'USD'
};
ui_parameters?: {
calendar_color?: string;
};
service_type: 'standalone' | 'variant' | 'bundle';
is_active: boolean;
parent_service_id?: string; // For variants
categories?: Category[];
merchant_id: string;
tenant_id: string;
inserted_at: string;
updated_at: string;
}

Category Object

interface Category {
id: string;
name: string;
description?: string;
order_index?: number;
is_active: boolean;
merchant_id: string;
tenant_id: string;
inserted_at: string;
updated_at: string;
}

Best Practices

Category Organization

Category TypeExamplesNotes
Service TypeHair, Nails, SkincareGroup by what you offer
DurationQuick Services, Full TreatmentsGroup by time commitment
Price TierEssential, Premium, LuxuryGroup by pricing

Naming Services

  • Use clear, client-friendly names
  • Include key differentiators (duration, style)
  • Avoid internal codes or abbreviations
// Good names
'Haircut - Women\'s'
'Deep Tissue Massage - 60 min'
'Gel Manicure with Art'

// Avoid
'HCW001'
'DTM1H'
'GMWA'

Bundle Pricing

// Calculate savings for bundles
const individualTotal = services.reduce((sum, s) =>
sum + (s.default_price?.amount || 0), 0
);
const bundlePrice = Math.round(individualTotal * 0.85); // 15% discount

const bundle = await ServicePackageService.servicePackageCreate({
requestBody: {
name: 'Spa Day Package (Save 15%)',
default_price: { amount: bundlePrice, currency: 'USD' },
// ...
}
});

Archive vs Delete

ActionUse WhenRecoverable
ArchiveSeasonal services, temporarily unavailableYes
DeleteNever offered, created by mistakeNo

Error Handling

try {
await ServiceService.serviceCreate({ requestBody: data });
} catch (error) {
if (error.status === 422) {
// Validation error - check error.body for details
console.error('Validation failed:', error.body.errors);
} else if (error.status === 404) {
// Resource not found (e.g., invalid merchant_id)
console.error('Not found:', error.body.error);
} else if (error.status === 401) {
// Authentication required
console.error('Please login first');
}
}

Next Steps