Files
smoothschedule/frontend/src/pages/HelpApiDocs.tsx
poduck c7f241b30a feat(i18n): Comprehensive internationalization of frontend components and pages
Translate all hardcoded English strings to use i18n translation keys:

Components:
- TransactionDetailModal: payment details, refunds, technical info
- ConnectOnboarding/ConnectOnboardingEmbed: Stripe Connect setup
- StripeApiKeysForm: API key management
- DomainPurchase: domain registration flow
- Sidebar: navigation labels
- Schedule/Sidebar, PendingSidebar: scheduler UI
- MasqueradeBanner: masquerade status
- Dashboard widgets: metrics, capacity, customers, tickets
- Marketing: PricingTable, PluginShowcase, BenefitsSection
- ConfirmationModal, ServiceList: common UI

Pages:
- Staff: invitation flow, role management
- Customers: form placeholders
- Payments: transactions, payouts, billing
- BookingSettings: URL and redirect configuration
- TrialExpired: upgrade prompts and features
- PlatformSettings, PlatformBusinesses: admin UI
- HelpApiDocs: API documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 21:40:54 -05:00

1787 lines
70 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import {
ArrowLeft,
Copy,
Check,
ChevronDown,
ChevronRight,
ExternalLink,
Key,
AlertCircle,
} from 'lucide-react';
import { useTestTokensForDocs } from '../hooks/useApiTokens';
import { API_BASE_URL } from '../api/config';
// =============================================================================
// TYPES & CONSTANTS
// =============================================================================
type CodeLanguage = 'curl' | 'python' | 'php' | 'ruby' | 'perl' | 'json' | 'http' | 'javascript' | 'java' | 'go' | 'csharp';
interface LanguageConfig {
label: string;
icon: string;
}
const LANGUAGES: Record<CodeLanguage, LanguageConfig> = {
curl: { label: 'cURL', icon: '>' },
python: { label: 'Python', icon: 'py' },
php: { label: 'PHP', icon: 'php' },
ruby: { label: 'Ruby', icon: 'rb' },
perl: { label: 'Perl', icon: 'pl' },
javascript: { label: 'Node.js', icon: 'js' },
java: { label: 'Java', icon: 'java' },
go: { label: 'Go', icon: 'go' },
csharp: { label: '.NET', icon: 'cs' },
json: { label: 'JSON', icon: '{}' },
http: { label: 'HTTP', icon: 'H' },
};
// Default test credentials (used when no tokens are available)
const DEFAULT_TEST_API_KEY = 'ss_test_<your_test_token_here>';
const DEFAULT_TEST_WEBHOOK_SECRET = 'whsec_test_abc123def456ghi789jkl012mno345pqr678';
const SANDBOX_URL = 'https://sandbox.smoothschedule.com/v1';
// Multi-language code interface
interface MultiLangCode {
curl: string;
python: string;
php: string;
ruby: string;
perl: string;
javascript: string;
java: string;
go: string;
csharp: string;
}
// =============================================================================
// CONTEXT FOR SHARED LANGUAGE STATE
// =============================================================================
const LanguageContext = React.createContext<{
activeLanguage: keyof MultiLangCode;
setActiveLanguage: (lang: keyof MultiLangCode) => void;
}>({
activeLanguage: 'curl',
setActiveLanguage: () => {},
});
// =============================================================================
// SYNTAX HIGHLIGHTING
// =============================================================================
const highlightSyntax = (code: string, language: CodeLanguage): React.ReactNode => {
const patterns: Record<string, Array<{ pattern: RegExp; className: string }>> = {
json: [
{ pattern: /"([^"\\]|\\.)*"(?=\s*:)/g, className: 'text-purple-400' },
{ pattern: /"([^"\\]|\\.)*"(?!\s*:)/g, className: 'text-green-400' },
{ pattern: /\b(true|false|null)\b/g, className: 'text-orange-400' },
{ pattern: /\b(-?\d+\.?\d*)\b/g, className: 'text-cyan-400' },
],
python: [
{ pattern: /#.*/g, className: 'text-gray-500 italic' },
{ pattern: /\b(import|from|class|def|return|if|else|elif|for|while|try|except|with|as|None|True|False|self|async|await)\b/g, className: 'text-purple-400' },
{ pattern: /('([^'\\]|\\.)*'|"([^"\\]|\\.)*"|f"([^"\\]|\\.)*"|f'([^'\\]|\\.)*')/g, className: 'text-green-400' },
{ pattern: /\b(\d+\.?\d*)\b/g, className: 'text-cyan-400' },
{ pattern: /\b([A-Z][a-zA-Z0-9_]*)\b/g, className: 'text-yellow-400' },
{ pattern: /@\w+/g, className: 'text-pink-400' },
],
php: [
{ pattern: /\/\/.*/g, className: 'text-gray-500 italic' },
{ pattern: /\b(class|function|public|private|protected|return|if|else|elseif|foreach|for|while|new|throw|try|catch|extends|implements|use|namespace|array|string|int|bool|void)\b/g, className: 'text-purple-400' },
{ pattern: /('([^'\\]|\\.)*'|"([^"\\]|\\.)*")/g, className: 'text-green-400' },
{ pattern: /\$[a-zA-Z_][a-zA-Z0-9_]*/g, className: 'text-orange-400' },
{ pattern: /\b(\d+\.?\d*)\b/g, className: 'text-cyan-400' },
{ pattern: /->/g, className: 'text-pink-400' },
],
ruby: [
{ pattern: /#.*/g, className: 'text-gray-500 italic' },
{ pattern: /\b(class|def|end|return|if|else|elsif|unless|while|do|require|module|attr_accessor|attr_reader|attr_writer|private|protected|public|new|nil|true|false|self|yield|raise|begin|rescue)\b/g, className: 'text-purple-400' },
{ pattern: /('([^'\\]|\\.)*'|"([^"\\]|\\.)*")/g, className: 'text-green-400' },
{ pattern: /:[a-zA-Z_][a-zA-Z0-9_]*/g, className: 'text-cyan-400' },
{ pattern: /@[a-zA-Z_][a-zA-Z0-9_]*/g, className: 'text-orange-400' },
{ pattern: /\b(\d+\.?\d*)\b/g, className: 'text-cyan-400' },
],
perl: [
{ pattern: /#.*/g, className: 'text-gray-500 italic' },
{ pattern: /\b(use|my|sub|return|if|else|elsif|unless|while|for|foreach|package|strict|warnings|die|print|say)\b/g, className: 'text-purple-400' },
{ pattern: /('([^'\\]|\\.)*'|"([^"\\]|\\.)*")/g, className: 'text-green-400' },
{ pattern: /\$[a-zA-Z_][a-zA-Z0-9_]*/g, className: 'text-orange-400' },
{ pattern: /@[a-zA-Z_][a-zA-Z0-9_]*/g, className: 'text-yellow-400' },
{ pattern: /%[a-zA-Z_][a-zA-Z0-9_]*/g, className: 'text-pink-400' },
{ pattern: /\b(\d+\.?\d*)\b/g, className: 'text-cyan-400' },
],
curl: [
{ pattern: /\bcurl\b/g, className: 'text-purple-400' },
{ pattern: /-[A-Za-z]+/g, className: 'text-cyan-400' },
{ pattern: /('([^'\\]|\\.)*'|"([^"\\]|\\.)*")/g, className: 'text-green-400' },
{ pattern: /https?:\/\/[^\s"']+/g, className: 'text-yellow-400' },
{ pattern: /\\$/gm, className: 'text-gray-500' },
],
javascript: [
{ pattern: /\/\/.*/g, className: 'text-gray-500 italic' },
{ pattern: /\b(const|let|var|function|class|return|if|else|for|while|async|await|new|this|import|export|from|default|throw|try|catch|extends|require|module)\b/g, className: 'text-purple-400' },
{ pattern: /(`[^`]*`|'([^'\\]|\\.)*'|"([^"\\]|\\.)*")/g, className: 'text-green-400' },
{ pattern: /\b(\d+\.?\d*)\b/g, className: 'text-cyan-400' },
{ pattern: /\b(true|false|null|undefined)\b/g, className: 'text-orange-400' },
],
java: [
{ pattern: /\/\/.*/g, className: 'text-gray-500 italic' },
{ pattern: /\b(public|private|protected|class|interface|extends|implements|static|final|void|return|if|else|for|while|try|catch|throw|throws|new|import|package|this|super)\b/g, className: 'text-purple-400' },
{ pattern: /("([^"\\]|\\.)*")/g, className: 'text-green-400' },
{ pattern: /\b(\d+\.?\d*[LlFfDd]?)\b/g, className: 'text-cyan-400' },
{ pattern: /\b(true|false|null)\b/g, className: 'text-orange-400' },
{ pattern: /\b(String|int|long|double|float|boolean|Integer|Long|Double|Map|List|HashMap|ArrayList|HttpClient|HttpRequest|HttpResponse|URI|JSONObject|JSONArray)\b/g, className: 'text-yellow-400' },
{ pattern: /@\w+/g, className: 'text-pink-400' },
],
go: [
{ pattern: /\/\/.*/g, className: 'text-gray-500 italic' },
{ pattern: /\b(package|import|func|return|if|else|for|range|var|const|type|struct|interface|map|chan|go|defer|select|case|default|break|continue|fallthrough|nil)\b/g, className: 'text-purple-400' },
{ pattern: /(`[^`]*`|"([^"\\]|\\.)*")/g, className: 'text-green-400' },
{ pattern: /\b(\d+\.?\d*)\b/g, className: 'text-cyan-400' },
{ pattern: /\b(true|false|nil)\b/g, className: 'text-orange-400' },
{ pattern: /\b(string|int|int64|float64|bool|error|byte|rune|any)\b/g, className: 'text-yellow-400' },
],
csharp: [
{ pattern: /\/\/.*/g, className: 'text-gray-500 italic' },
{ pattern: /\b(using|namespace|class|public|private|protected|static|async|await|return|if|else|for|foreach|while|try|catch|throw|new|var|void|get|set|this|base)\b/g, className: 'text-purple-400' },
{ pattern: /("([^"\\]|\\.)*"|\$"([^"\\]|\\.)*")/g, className: 'text-green-400' },
{ pattern: /\b(\d+\.?\d*[MmFfDdLl]?)\b/g, className: 'text-cyan-400' },
{ pattern: /\b(true|false|null)\b/g, className: 'text-orange-400' },
{ pattern: /\b(string|int|long|double|float|bool|object|Task|HttpClient|HttpRequestMessage|HttpResponseMessage|StringContent|JsonSerializer|Dictionary|List)\b/g, className: 'text-yellow-400' },
{ pattern: /\[\w+\]/g, className: 'text-pink-400' },
],
http: [
{ pattern: /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/gm, className: 'text-purple-400 font-bold' },
{ pattern: /^[A-Za-z-]+(?=:)/gm, className: 'text-cyan-400' },
{ pattern: /Bearer\s+\S+/g, className: 'text-green-400' },
{ pattern: /\b(\d+)\b/g, className: 'text-orange-400' },
],
};
const langPatterns = patterns[language] || [];
if (langPatterns.length === 0) {
return <span>{code}</span>;
}
const lines = code.split('\n');
return (
<>
{lines.map((line, lineIndex) => {
let result: Array<{ text: string; className?: string; start: number }> = [{ text: line, start: 0 }];
langPatterns.forEach(({ pattern, className }) => {
const newResult: typeof result = [];
result.forEach(segment => {
if (segment.className) {
newResult.push(segment);
return;
}
const text = segment.text;
const regex = new RegExp(pattern.source, pattern.flags);
let lastIndex = 0;
let match;
while ((match = regex.exec(text)) !== null) {
if (match.index > lastIndex) {
newResult.push({ text: text.slice(lastIndex, match.index), start: segment.start + lastIndex });
}
newResult.push({ text: match[0], className, start: segment.start + match.index });
lastIndex = match.index + match[0].length;
if (match[0].length === 0) break;
}
if (lastIndex < text.length) {
newResult.push({ text: text.slice(lastIndex), start: segment.start + lastIndex });
}
});
result = newResult.length > 0 ? newResult : result;
});
return (
<React.Fragment key={lineIndex}>
{result.map((segment, i) => (
<span key={i} className={segment.className}>{segment.text}</span>
))}
{lineIndex < lines.length - 1 && '\n'}
</React.Fragment>
);
})}
</>
);
};
// =============================================================================
// CODE BLOCK COMPONENTS
// =============================================================================
const CodeBlock: React.FC<{ code: string; language?: CodeLanguage; title?: string }> = ({
code,
language = 'json',
title
}) => {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="rounded-lg overflow-hidden bg-[#0a0a0f] mb-4">
{title && (
<div className="px-4 py-2 text-xs text-gray-400 border-b border-gray-800 flex items-center justify-between">
<span>{title}</span>
<span className="text-gray-500">{LANGUAGES[language]?.label || language}</span>
</div>
)}
<div className="relative">
<pre className="p-4 overflow-x-auto text-sm font-mono leading-relaxed text-gray-300">
<code>{highlightSyntax(code, language)}</code>
</pre>
<button
onClick={handleCopy}
className="absolute top-2 right-2 p-1.5 rounded bg-gray-800 hover:bg-gray-700 text-gray-400 transition-colors"
title="Copy to clipboard"
>
{copied ? <Check size={14} /> : <Copy size={14} />}
</button>
</div>
</div>
);
};
const TabbedCodeBlock: React.FC<{
codes: MultiLangCode;
title?: string;
}> = ({ codes, title }) => {
const { activeLanguage, setActiveLanguage } = React.useContext(LanguageContext);
const [copied, setCopied] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(codes[activeLanguage]);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const tabs: Array<keyof MultiLangCode> = ['curl', 'javascript', 'python', 'go', 'java', 'csharp', 'php', 'ruby', 'perl'];
return (
<div className="rounded-lg overflow-hidden bg-[#0a0a0f] mb-4">
{title && (
<div className="px-4 py-2 text-xs text-gray-400 border-b border-gray-800 bg-gray-900/50">
{title}
</div>
)}
<div className="flex overflow-x-auto bg-[#12121a] border-b border-gray-800">
{tabs.map((lang) => (
<button
key={lang}
onClick={() => setActiveLanguage(lang)}
className={`px-4 py-2.5 text-xs font-medium transition-colors whitespace-nowrap border-b-2 ${
activeLanguage === lang
? 'text-white bg-[#1a1a24] border-purple-500'
: 'text-gray-400 hover:text-gray-200 border-transparent hover:bg-[#1a1a24]/50'
}`}
>
{LANGUAGES[lang].label}
</button>
))}
</div>
<div className="relative">
<pre className="p-4 overflow-x-auto text-sm font-mono leading-relaxed text-gray-300">
<code>{highlightSyntax(codes[activeLanguage], activeLanguage)}</code>
</pre>
<button
onClick={handleCopy}
className="absolute top-2 right-2 p-1.5 rounded bg-gray-800 hover:bg-gray-700 text-gray-400 transition-colors"
title="Copy to clipboard"
>
{copied ? <Check size={14} /> : <Copy size={14} />}
</button>
</div>
</div>
);
};
// =============================================================================
// SIDEBAR NAVIGATION
// =============================================================================
interface NavSection {
titleKey: string;
id: string;
items?: { titleKey: string; id: string; method?: 'GET' | 'POST' | 'PATCH' | 'DELETE' }[];
}
const navSections: NavSection[] = [
{ titleKey: 'help.api.introduction', id: 'introduction' },
{ titleKey: 'help.api.authentication', id: 'authentication' },
{ titleKey: 'help.api.errors', id: 'errors' },
{ titleKey: 'help.api.rateLimits', id: 'rate-limits' },
{
titleKey: 'help.api.business',
id: 'business',
items: [
{ titleKey: 'help.api.businessObject', id: 'business-object' },
{ titleKey: 'help.api.retrieveBusiness', id: 'retrieve-business', method: 'GET' },
],
},
{
titleKey: 'help.api.services',
id: 'services',
items: [
{ titleKey: 'help.api.serviceObject', id: 'service-object' },
{ titleKey: 'help.api.listServices', id: 'list-services', method: 'GET' },
{ titleKey: 'help.api.retrieveService', id: 'retrieve-service', method: 'GET' },
],
},
{
titleKey: 'help.api.resources',
id: 'resources',
items: [
{ titleKey: 'help.api.resourceObject', id: 'resource-object' },
{ titleKey: 'help.api.listResources', id: 'list-resources', method: 'GET' },
{ titleKey: 'help.api.retrieveResource', id: 'retrieve-resource', method: 'GET' },
],
},
{
titleKey: 'help.api.availability',
id: 'availability',
items: [
{ titleKey: 'help.api.checkAvailability', id: 'check-availability', method: 'GET' },
],
},
{
titleKey: 'help.api.appointments',
id: 'appointments',
items: [
{ titleKey: 'help.api.appointmentObject', id: 'appointment-object' },
{ titleKey: 'help.api.createAppointment', id: 'create-appointment', method: 'POST' },
{ titleKey: 'help.api.retrieveAppointment', id: 'retrieve-appointment', method: 'GET' },
{ titleKey: 'help.api.updateAppointment', id: 'update-appointment', method: 'PATCH' },
{ titleKey: 'help.api.cancelAppointment', id: 'cancel-appointment', method: 'DELETE' },
{ titleKey: 'help.api.listAppointments', id: 'list-appointments', method: 'GET' },
],
},
{
titleKey: 'help.api.customers',
id: 'customers',
items: [
{ titleKey: 'help.api.customerObject', id: 'customer-object' },
{ titleKey: 'help.api.createCustomer', id: 'create-customer', method: 'POST' },
{ titleKey: 'help.api.retrieveCustomer', id: 'retrieve-customer', method: 'GET' },
{ titleKey: 'help.api.updateCustomer', id: 'update-customer', method: 'PATCH' },
{ titleKey: 'help.api.listCustomers', id: 'list-customers', method: 'GET' },
],
},
{
titleKey: 'help.api.webhooks',
id: 'webhooks',
items: [
{ titleKey: 'help.api.webhookEvents', id: 'webhook-events' },
{ titleKey: 'help.api.createWebhook', id: 'create-webhook', method: 'POST' },
{ titleKey: 'help.api.listWebhooks', id: 'list-webhooks', method: 'GET' },
{ titleKey: 'help.api.deleteWebhook', id: 'delete-webhook', method: 'DELETE' },
{ titleKey: 'help.api.verifySignatures', id: 'verify-signatures' },
],
},
];
const MethodBadge: React.FC<{ method: 'GET' | 'POST' | 'PATCH' | 'DELETE' }> = ({ method }) => {
const colors = {
GET: 'text-green-400',
POST: 'text-blue-400',
PATCH: 'text-yellow-400',
DELETE: 'text-red-400',
};
return (
<span className={`text-[10px] font-mono font-bold ${colors[method]}`}>
{method}
</span>
);
};
const Sidebar: React.FC<{
activeSection: string;
onSectionClick: (id: string) => void;
t: (key: string) => string;
}> = ({
activeSection,
onSectionClick,
t,
}) => {
const [expandedSections, setExpandedSections] = useState<string[]>(['business', 'services', 'appointments']);
const toggleSection = (id: string) => {
setExpandedSections(prev =>
prev.includes(id) ? prev.filter(s => s !== id) : [...prev, id]
);
};
return (
<nav className="w-64 flex-shrink-0 border-r border-gray-200 dark:border-gray-800 sticky top-[65px] h-[calc(100vh-65px)] overflow-y-auto">
<div className="p-4 space-y-1">
{navSections.map(section => (
<div key={section.id}>
{section.items ? (
<>
<button
onClick={() => toggleSection(section.id)}
className="w-full flex items-center justify-between px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md"
>
{t(section.titleKey)}
{expandedSections.includes(section.id) ? (
<ChevronDown size={16} />
) : (
<ChevronRight size={16} />
)}
</button>
{expandedSections.includes(section.id) && (
<div className="ml-3 mt-1 space-y-0.5">
{section.items.map(item => (
<button
key={item.id}
onClick={() => onSectionClick(item.id)}
className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm rounded-md transition-colors ${
activeSection === item.id
? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
>
{item.method && <MethodBadge method={item.method} />}
<span className="truncate">{t(item.titleKey)}</span>
</button>
))}
</div>
)}
</>
) : (
<button
onClick={() => onSectionClick(section.id)}
className={`w-full flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors ${
activeSection === section.id
? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
>
{t(section.titleKey)}
</button>
)}
</div>
))}
</div>
</nav>
);
};
// =============================================================================
// API SECTION COMPONENT (Stripe-style split pane)
// =============================================================================
interface ApiSectionProps {
id: string;
children: React.ReactNode;
}
const ApiSection: React.FC<ApiSectionProps> = ({ id, children }) => {
return (
<section id={id} className="py-12 border-b border-gray-200 dark:border-gray-800 last:border-b-0 scroll-mt-24">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{children}
</div>
</section>
);
};
const ApiContent: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="prose prose-gray dark:prose-invert max-w-none">
{children}
</div>
);
const ApiExample: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="lg:sticky lg:top-[81px] z-10">
{children}
</div>
);
// =============================================================================
// ATTRIBUTE TABLE
// =============================================================================
interface Attribute {
name: string;
type: string;
description: string;
required?: boolean;
}
const AttributeTable: React.FC<{ attributes: Attribute[] }> = ({ attributes }) => (
<div className="mt-4 space-y-3">
{attributes.map(attr => (
<div key={attr.name} className="border-b border-gray-100 dark:border-gray-800 pb-3 last:border-b-0">
<div className="flex items-center gap-2">
<code className="text-sm font-mono text-purple-600 dark:text-purple-400">{attr.name}</code>
<span className="text-xs text-gray-500">{attr.type}</span>
{attr.required && (
<span className="text-xs text-red-500 font-medium">required</span>
)}
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{attr.description}</p>
</div>
))}
</div>
);
// =============================================================================
// MAIN COMPONENT
// =============================================================================
const HelpApiDocs: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [activeSection, setActiveSection] = useState('introduction');
const [activeLanguage, setActiveLanguage] = useState<keyof MultiLangCode>('curl');
const [selectedTokenId, setSelectedTokenId] = useState<string | null>(null);
// Fetch test tokens
const { data: testTokens, isLoading: tokensLoading, error: tokensError } = useTestTokensForDocs();
// Get the currently selected token or the first one
const selectedToken = testTokens?.find(t => t.id === selectedTokenId) || testTokens?.[0];
// Auto-select first token when tokens load
useEffect(() => {
if (testTokens && testTokens.length > 0 && !selectedTokenId) {
setSelectedTokenId(testTokens[0].id);
}
}, [testTokens, selectedTokenId]);
// Get the API key to use in examples (either from selected token or default)
const TEST_API_KEY = selectedToken?.key_prefix || DEFAULT_TEST_API_KEY;
const TEST_WEBHOOK_SECRET = DEFAULT_TEST_WEBHOOK_SECRET;
const handleSectionClick = (id: string) => {
setActiveSection(id);
const element = document.getElementById(id);
if (element) {
// Use scrollIntoView with the CSS scroll-mt-24 (96px margin)
// This respects the scroll-margin-top CSS property we set on sections
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
};
// Track active section on scroll
useEffect(() => {
const handleScroll = () => {
const sections = document.querySelectorAll('section[id]');
let current = 'introduction';
const headerHeight = 72;
const threshold = headerHeight + 32; // Header height + some padding
sections.forEach(section => {
const rect = section.getBoundingClientRect();
if (rect.top <= threshold) {
current = section.id;
}
});
setActiveSection(current);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// =============================================================================
// CODE SNIPPETS
// =============================================================================
const getServicesCode: MultiLangCode = {
curl: `curl ${SANDBOX_URL}/services/ \\
-H "Authorization: Bearer ${TEST_API_KEY}"`,
javascript: `const response = await fetch('${SANDBOX_URL}/services/', {
headers: {
'Authorization': 'Bearer ${TEST_API_KEY}'
}
});
const services = await response.json();`,
python: `import requests
response = requests.get(
'${SANDBOX_URL}/services/',
headers={'Authorization': 'Bearer ${TEST_API_KEY}'}
)
services = response.json()`,
go: `req, _ := http.NewRequest("GET", "${SANDBOX_URL}/services/", nil)
req.Header.Set("Authorization", "Bearer ${TEST_API_KEY}")
resp, _ := http.DefaultClient.Do(req)`,
java: `HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("${SANDBOX_URL}/services/"))
.header("Authorization", "Bearer ${TEST_API_KEY}")
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());`,
csharp: `var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "${TEST_API_KEY}");
var response = await client.GetAsync("${SANDBOX_URL}/services/");`,
php: `$ch = curl_init('${SANDBOX_URL}/services/');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ${TEST_API_KEY}'
]);
$response = curl_exec($ch);`,
ruby: `response = Net::HTTP.get_response(
URI('${SANDBOX_URL}/services/'),
{ 'Authorization' => 'Bearer ${TEST_API_KEY}' }
)`,
perl: `my $ua = LWP::UserAgent->new;
my $response = $ua->get('${SANDBOX_URL}/services/',
'Authorization' => 'Bearer ${TEST_API_KEY}'
);`,
};
const getAvailabilityCode: MultiLangCode = {
curl: `curl "${SANDBOX_URL}/availability/?service_id=svc_123&date=2025-12-01" \\
-H "Authorization: Bearer ${TEST_API_KEY}"`,
javascript: `const response = await fetch(
'${SANDBOX_URL}/availability/?service_id=svc_123&date=2025-12-01',
{
headers: {
'Authorization': 'Bearer ${TEST_API_KEY}'
}
}
);
const availability = await response.json();`,
python: `import requests
response = requests.get(
'${SANDBOX_URL}/availability/',
params={'service_id': 'svc_123', 'date': '2025-12-01'},
headers={'Authorization': 'Bearer ${TEST_API_KEY}'}
)
availability = response.json()`,
go: `req, _ := http.NewRequest("GET",
"${SANDBOX_URL}/availability/?service_id=svc_123&date=2025-12-01", nil)
req.Header.Set("Authorization", "Bearer ${TEST_API_KEY}")
resp, _ := http.DefaultClient.Do(req)`,
java: `HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("${SANDBOX_URL}/availability/?service_id=svc_123&date=2025-12-01"))
.header("Authorization", "Bearer ${TEST_API_KEY}")
.build();`,
csharp: `var response = await client.GetAsync(
"${SANDBOX_URL}/availability/?service_id=svc_123&date=2025-12-01");`,
php: `$ch = curl_init('${SANDBOX_URL}/availability/?service_id=svc_123&date=2025-12-01');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ${TEST_API_KEY}'
]);`,
ruby: `uri = URI('${SANDBOX_URL}/availability/')
uri.query = URI.encode_www_form(service_id: 'svc_123', date: '2025-12-01')
response = Net::HTTP.get_response(uri, { 'Authorization' => 'Bearer ${TEST_API_KEY}' })`,
perl: `my $response = $ua->get(
'${SANDBOX_URL}/availability/?service_id=svc_123&date=2025-12-01',
'Authorization' => 'Bearer ${TEST_API_KEY}'
);`,
};
const createAppointmentCode: MultiLangCode = {
curl: `curl ${SANDBOX_URL}/appointments/ \\
-H "Authorization: Bearer ${TEST_API_KEY}" \\
-H "Content-Type: application/json" \\
-d '{
"service_id": "svc_123",
"resource_id": "res_456",
"start_time": "2025-12-01T10:00:00Z",
"customer_email": "john@example.com",
"customer_name": "John Doe"
}'`,
javascript: `const response = await fetch('${SANDBOX_URL}/appointments/', {
method: 'POST',
headers: {
'Authorization': 'Bearer ${TEST_API_KEY}',
'Content-Type': 'application/json'
},
body: JSON.stringify({
service_id: 'svc_123',
resource_id: 'res_456',
start_time: '2025-12-01T10:00:00Z',
customer_email: 'john@example.com',
customer_name: 'John Doe'
})
});`,
python: `import requests
response = requests.post(
'${SANDBOX_URL}/appointments/',
headers={'Authorization': 'Bearer ${TEST_API_KEY}'},
json={
'service_id': 'svc_123',
'resource_id': 'res_456',
'start_time': '2025-12-01T10:00:00Z',
'customer_email': 'john@example.com',
'customer_name': 'John Doe'
}
)`,
go: `body := strings.NewReader(\`{
"service_id": "svc_123",
"resource_id": "res_456",
"start_time": "2025-12-01T10:00:00Z",
"customer_email": "john@example.com",
"customer_name": "John Doe"
}\`)
req, _ := http.NewRequest("POST", "${SANDBOX_URL}/appointments/", body)
req.Header.Set("Authorization", "Bearer ${TEST_API_KEY}")
req.Header.Set("Content-Type", "application/json")`,
java: `String json = """
{
"service_id": "svc_123",
"resource_id": "res_456",
"start_time": "2025-12-01T10:00:00Z",
"customer_email": "john@example.com",
"customer_name": "John Doe"
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("${SANDBOX_URL}/appointments/"))
.header("Authorization", "Bearer ${TEST_API_KEY}")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();`,
csharp: `var content = new StringContent(
JsonSerializer.Serialize(new {
service_id = "svc_123",
resource_id = "res_456",
start_time = "2025-12-01T10:00:00Z",
customer_email = "john@example.com",
customer_name = "John Doe"
}),
Encoding.UTF8,
"application/json"
);
var response = await client.PostAsync("${SANDBOX_URL}/appointments/", content);`,
php: `$data = [
'service_id' => 'svc_123',
'resource_id' => 'res_456',
'start_time' => '2025-12-01T10:00:00Z',
'customer_email' => 'john@example.com',
'customer_name' => 'John Doe'
];
$ch = curl_init('${SANDBOX_URL}/appointments/');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ${TEST_API_KEY}',
'Content-Type: application/json'
]
]);`,
ruby: `uri = URI('${SANDBOX_URL}/appointments/')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['Authorization'] = 'Bearer ${TEST_API_KEY}'
request['Content-Type'] = 'application/json'
request.body = {
service_id: 'svc_123',
resource_id: 'res_456',
start_time: '2025-12-01T10:00:00Z',
customer_email: 'john@example.com',
customer_name: 'John Doe'
}.to_json`,
perl: `my $ua = LWP::UserAgent->new;
my $req = HTTP::Request->new(POST => '${SANDBOX_URL}/appointments/');
$req->header('Authorization' => 'Bearer ${TEST_API_KEY}');
$req->header('Content-Type' => 'application/json');
$req->content(encode_json({
service_id => 'svc_123',
resource_id => 'res_456',
start_time => '2025-12-01T10:00:00Z',
customer_email => 'john@example.com',
customer_name => 'John Doe'
}));`,
};
const createWebhookCode: MultiLangCode = {
curl: `curl ${SANDBOX_URL}/webhooks/ \\
-H "Authorization: Bearer ${TEST_API_KEY}" \\
-H "Content-Type: application/json" \\
-d '{
"url": "https://example.com/webhooks",
"events": ["appointment.created", "appointment.cancelled"]
}'`,
javascript: `const response = await fetch('${SANDBOX_URL}/webhooks/', {
method: 'POST',
headers: {
'Authorization': 'Bearer ${TEST_API_KEY}',
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://example.com/webhooks',
events: ['appointment.created', 'appointment.cancelled']
})
});
// Save the secret from the response - it's only shown once!`,
python: `response = requests.post(
'${SANDBOX_URL}/webhooks/',
headers={'Authorization': 'Bearer ${TEST_API_KEY}'},
json={
'url': 'https://example.com/webhooks',
'events': ['appointment.created', 'appointment.cancelled']
}
)
# Save the secret from the response - it's only shown once!`,
go: `body := strings.NewReader(\`{
"url": "https://example.com/webhooks",
"events": ["appointment.created", "appointment.cancelled"]
}\`)
req, _ := http.NewRequest("POST", "${SANDBOX_URL}/webhooks/", body)
req.Header.Set("Authorization", "Bearer ${TEST_API_KEY}")
req.Header.Set("Content-Type", "application/json")
// Save the secret from the response - it's only shown once!`,
java: `String json = """
{
"url": "https://example.com/webhooks",
"events": ["appointment.created", "appointment.cancelled"]
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("${SANDBOX_URL}/webhooks/"))
.header("Authorization", "Bearer ${TEST_API_KEY}")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
// Save the secret from the response - it's only shown once!`,
csharp: `var content = new StringContent(
JsonSerializer.Serialize(new {
url = "https://example.com/webhooks",
events = new[] { "appointment.created", "appointment.cancelled" }
}),
Encoding.UTF8,
"application/json"
);
var response = await client.PostAsync("${SANDBOX_URL}/webhooks/", content);
// Save the secret from the response - it's only shown once!`,
php: `$data = [
'url' => 'https://example.com/webhooks',
'events' => ['appointment.created', 'appointment.cancelled']
];
$ch = curl_init('${SANDBOX_URL}/webhooks/');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ${TEST_API_KEY}',
'Content-Type: application/json'
]
]);
// Save the secret from the response - it's only shown once!`,
ruby: `request = Net::HTTP::Post.new(URI('${SANDBOX_URL}/webhooks/'))
request['Authorization'] = 'Bearer ${TEST_API_KEY}'
request['Content-Type'] = 'application/json'
request.body = {
url: 'https://example.com/webhooks',
events: ['appointment.created', 'appointment.cancelled']
}.to_json
# Save the secret from the response - it's only shown once!`,
perl: `my $req = HTTP::Request->new(POST => '${SANDBOX_URL}/webhooks/');
$req->header('Authorization' => 'Bearer ${TEST_API_KEY}');
$req->content(encode_json({
url => 'https://example.com/webhooks',
events => ['appointment.created', 'appointment.cancelled']
}));
# Save the secret from the response - it's only shown once!`,
};
const verifyWebhookCode: MultiLangCode = {
curl: `# Verify the signature from the X-Webhook-Signature header
# Format: <timestamp>.<signature>
echo -n "<timestamp>.<payload>" | \\
openssl dgst -sha256 -hmac "${TEST_WEBHOOK_SECRET}"`,
javascript: `const crypto = require('crypto');
function verifyWebhook(payload, header, secret) {
const [timestamp, signature] = header.split('.');
const expected = crypto
.createHmac('sha256', secret)
.update(\`\${timestamp}.\${payload}\`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your webhook handler:
const isValid = verifyWebhook(
req.body,
req.headers['x-webhook-signature'],
'${TEST_WEBHOOK_SECRET}'
);`,
python: `import hmac
import hashlib
def verify_webhook(payload, header, secret):
timestamp, signature = header.split('.')
message = f"{timestamp}.{payload}"
expected = hmac.new(
secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# In your webhook handler:
is_valid = verify_webhook(
request.body,
request.headers['X-Webhook-Signature'],
'${TEST_WEBHOOK_SECRET}'
)`,
go: `import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strings"
)
func verifyWebhook(payload, header, secret string) bool {
parts := strings.SplitN(header, ".", 2)
timestamp, signature := parts[0], parts[1]
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp + "." + payload))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}`,
java: `import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public boolean verifyWebhook(String payload, String header, String secret) {
String[] parts = header.split("\\\\.", 2);
String timestamp = parts[0];
String signature = parts[1];
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
String expected = bytesToHex(mac.doFinal((timestamp + "." + payload).getBytes()));
return MessageDigest.isEqual(signature.getBytes(), expected.getBytes());
}`,
csharp: `using System.Security.Cryptography;
bool VerifyWebhook(string payload, string header, string secret) {
var parts = header.Split('.', 2);
var timestamp = parts[0];
var signature = parts[1];
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{payload}"));
var expected = BitConverter.ToString(hash).Replace("-", "").ToLower();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(signature),
Encoding.UTF8.GetBytes(expected)
);
}`,
php: `function verifyWebhook($payload, $header, $secret) {
[$timestamp, $signature] = explode('.', $header, 2);
$expected = hash_hmac('sha256', "{$timestamp}.{$payload}", $secret);
return hash_equals($expected, $signature);
}
// In your webhook handler:
$isValid = verifyWebhook(
file_get_contents('php://input'),
$_SERVER['HTTP_X_WEBHOOK_SIGNATURE'],
'${TEST_WEBHOOK_SECRET}'
);`,
ruby: `require 'openssl'
def verify_webhook(payload, header, secret)
timestamp, signature = header.split('.', 2)
expected = OpenSSL::HMAC.hexdigest('sha256', secret, "#{timestamp}.#{payload}")
Rack::Utils.secure_compare(expected, signature)
end
# In your webhook handler:
is_valid = verify_webhook(
request.body.read,
request.env['HTTP_X_WEBHOOK_SIGNATURE'],
'${TEST_WEBHOOK_SECRET}'
)`,
perl: `use Digest::SHA qw(hmac_sha256_hex);
sub verify_webhook {
my ($payload, $header, $secret) = @_;
my ($timestamp, $signature) = split /\\./, $header, 2;
my $expected = hmac_sha256_hex("$timestamp.$payload", $secret);
return $expected eq $signature;
}`,
};
const authExampleCode: MultiLangCode = {
curl: `curl ${SANDBOX_URL}/services/ \\
-H "Authorization: Bearer ${TEST_API_KEY}"`,
javascript: `const response = await fetch('${SANDBOX_URL}/services/', {
headers: {
'Authorization': 'Bearer ${TEST_API_KEY}'
}
});`,
python: `import requests
response = requests.get(
'${SANDBOX_URL}/services/',
headers={'Authorization': 'Bearer ${TEST_API_KEY}'}
)`,
go: `req, _ := http.NewRequest("GET", "${SANDBOX_URL}/services/", nil)
req.Header.Set("Authorization", "Bearer ${TEST_API_KEY}")
resp, _ := http.DefaultClient.Do(req)`,
java: `HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("${SANDBOX_URL}/services/"))
.header("Authorization", "Bearer ${TEST_API_KEY}")
.build();`,
csharp: `var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "${TEST_API_KEY}");
var response = await client.GetAsync("${SANDBOX_URL}/services/");`,
php: `$ch = curl_init('${SANDBOX_URL}/services/');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ${TEST_API_KEY}'
]);
$response = curl_exec($ch);`,
ruby: `uri = URI('${SANDBOX_URL}/services/')
request = Net::HTTP::Get.new(uri)
request['Authorization'] = 'Bearer ${TEST_API_KEY}'
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }`,
perl: `my $ua = LWP::UserAgent->new;
my $response = $ua->get('${SANDBOX_URL}/services/',
'Authorization' => 'Bearer ${TEST_API_KEY}'
);`,
};
return (
<LanguageContext.Provider value={{ activeLanguage, setActiveLanguage }}>
<div className="min-h-screen bg-white dark:bg-gray-900">
{/* Header */}
<header className="sticky top-0 z-50 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
<div className="flex items-center justify-between px-6 py-4">
<div className="flex items-center gap-4">
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors"
>
<ArrowLeft size={20} />
<span className="text-sm">{t('common.back', 'Back')}</span>
</button>
<div className="h-6 w-px bg-gray-200 dark:bg-gray-700" />
<h1 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.title')}
</h1>
</div>
<div className="flex items-center gap-4">
{/* Test Token Selector */}
{testTokens && testTokens.length > 0 && (
<div className="flex items-center gap-2">
<Key size={16} className="text-gray-400" />
<select
value={selectedTokenId || ''}
onChange={(e) => setSelectedTokenId(e.target.value)}
className="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{testTokens.map((token) => (
<option key={token.id} value={token.id}>
{token.name} ({token.key_prefix}...)
</option>
))}
</select>
</div>
)}
<a
href={`${API_BASE_URL}/v1/docs/`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-purple-600 dark:text-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20 rounded-lg transition-colors"
>
<ExternalLink size={16} />
{t('help.api.interactiveExplorer')}
</a>
</div>
</div>
</header>
{/* No Test Tokens Banner */}
{!tokensLoading && (!testTokens || testTokens.length === 0) && (
<div className="mx-6 mt-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
<div className="flex items-start gap-3">
<AlertCircle size={20} className="text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
<div>
<h3 className="text-sm font-semibold text-yellow-800 dark:text-yellow-200">
{t('help.api.noTestTokensFound')}
</h3>
<p className="text-sm text-yellow-700 dark:text-yellow-300 mt-1" dangerouslySetInnerHTML={{ __html: t('help.api.noTestTokensMessage') }} />
</div>
</div>
</div>
)}
{/* Token Loading Error */}
{tokensError && (
<div className="mx-6 mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<div className="flex items-start gap-3">
<AlertCircle size={20} className="text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
<div>
<h3 className="text-sm font-semibold text-red-800 dark:text-red-200">
{t('help.api.errorLoadingTokens')}
</h3>
<p className="text-sm text-red-700 dark:text-red-300 mt-1">
{t('help.api.errorLoadingTokensMessage')}
</p>
</div>
</div>
</div>
)}
<div className="flex relative">
{/* Sidebar */}
<Sidebar activeSection={activeSection} onSectionClick={handleSectionClick} t={t} />
{/* Main Content */}
<main className="flex-1 min-w-0 px-8 lg:px-12">
{/* Introduction */}
<ApiSection id="introduction">
<ApiContent>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
{t('help.api.introduction')}
</h2>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.introDescription')}
</p>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.introTestMode')}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.baseUrl')}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.baseUrlDescription')}
</p>
</ApiContent>
<ApiExample>
<CodeBlock
title={t('help.api.baseUrl')}
language="http"
code={`https://api.smoothschedule.com/v1/
# Sandbox/Test environment
${SANDBOX_URL}`}
/>
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong>{t('help.api.sandboxMode')}</strong> {t('help.api.sandboxModeDescription')}
</p>
</div>
</ApiExample>
</ApiSection>
{/* Authentication */}
<ApiSection id="authentication">
<ApiContent>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
{t('help.api.authentication')}
</h2>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.authDescription')}
</p>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.authBearer')}
</p>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.authWarning')}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.apiKeyFormat')}
</h3>
<ul className="text-gray-600 dark:text-gray-300">
<li><code>ss_test_*</code> {t('help.api.testKey')}</li>
<li><code>ss_live_*</code> {t('help.api.liveKey')}</li>
</ul>
</ApiContent>
<ApiExample>
<TabbedCodeBlock
title={t('help.api.authenticatedRequest')}
codes={authExampleCode}
/>
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
<p className="text-sm text-yellow-800 dark:text-yellow-200">
<strong>{t('help.api.keepKeysSecret')}</strong> {t('help.api.keepKeysSecretDescription')}
</p>
</div>
</ApiExample>
</ApiSection>
{/* Errors */}
<ApiSection id="errors">
<ApiContent>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
{t('help.api.errors')}
</h2>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.errorsDescription')}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.httpStatusCodes')}
</h3>
<AttributeTable
attributes={[
{ name: '200 - OK', type: '', description: t('help.api.statusOk') },
{ name: '201 - Created', type: '', description: t('help.api.statusCreated') },
{ name: '400 - Bad Request', type: '', description: t('help.api.statusBadRequest') },
{ name: '401 - Unauthorized', type: '', description: t('help.api.statusUnauthorized') },
{ name: '403 - Forbidden', type: '', description: t('help.api.statusForbidden') },
{ name: '404 - Not Found', type: '', description: t('help.api.statusNotFound') },
{ name: '409 - Conflict', type: '', description: t('help.api.statusConflict') },
{ name: '429 - Too Many Requests', type: '', description: t('help.api.statusTooManyRequests') },
{ name: '500 - Server Error', type: '', description: t('help.api.statusServerError') },
]}
/>
</ApiContent>
<ApiExample>
<CodeBlock
title={t('help.api.errorResponse')}
language="json"
code={`{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"details": {
"start_time": ["This field is required."],
"service_id": ["Invalid service ID."]
}
}
}`}
/>
</ApiExample>
</ApiSection>
{/* Rate Limits */}
<ApiSection id="rate-limits">
<ApiContent>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
{t('help.api.rateLimits')}
</h2>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.rateLimitsDescription')}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.limits')}
</h3>
<ul className="text-gray-600 dark:text-gray-300">
<li><strong>1,000</strong> {t('help.api.requestsPerHour')}</li>
<li><strong>100</strong> {t('help.api.requestsPerMinute')}</li>
</ul>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.rateLimitHeaders')}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.rateLimitHeadersDescription')}
</p>
</ApiContent>
<ApiExample>
<CodeBlock
title={t('help.api.rateLimitHeaders')}
language="http"
code={`X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1732795200
X-RateLimit-Burst-Limit: 100
X-RateLimit-Burst-Remaining: 95`}
/>
<CodeBlock
title="429 RESPONSE"
language="json"
code={`{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded",
"retry_after": 120
}
}`}
/>
</ApiExample>
</ApiSection>
{/* Business Object */}
<ApiSection id="business-object">
<ApiContent>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
{t('help.api.businessObject')}
</h2>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.businessObjectDescription')}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.attributes')}
</h3>
<AttributeTable
attributes={[
{ name: 'id', type: 'string', description: 'Unique identifier for the business.' },
{ name: 'name', type: 'string', description: 'The business name.' },
{ name: 'subdomain', type: 'string', description: 'The subdomain for this business.' },
{ name: 'logo_url', type: 'string', description: 'URL of the business logo.' },
{ name: 'primary_color', type: 'string', description: 'Primary brand color (hex).' },
{ name: 'timezone', type: 'string', description: 'Business timezone (IANA format).' },
{ name: 'cancellation_window_hours', type: 'integer', description: 'Hours before appointment when cancellation is allowed.' },
]}
/>
</ApiContent>
<ApiExample>
<CodeBlock
title="THE BUSINESS OBJECT"
language="json"
code={`{
"id": "biz_a1b2c3d4e5f6",
"name": "Acme Salon",
"subdomain": "acme",
"logo_url": "https://cdn.smoothschedule.com/logos/acme.png",
"primary_color": "#3B82F6",
"secondary_color": "#1E40AF",
"timezone": "America/New_York",
"cancellation_window_hours": 24
}`}
/>
</ApiExample>
</ApiSection>
{/* Retrieve Business */}
<ApiSection id="retrieve-business">
<ApiContent>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0 flex items-center gap-2">
<span className="text-xs px-2 py-0.5 rounded bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 font-mono">
GET
</span>
{t('help.api.retrieveBusiness')}
</h2>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.retrieveBusinessDescription')}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.requiredScope')}
</h3>
<p className="text-gray-600 dark:text-gray-300">
<code>business:read</code>
</p>
</ApiContent>
<ApiExample>
<CodeBlock
title={t('help.api.endpoint')}
language="http"
code="GET /v1/business/"
/>
<TabbedCodeBlock title={t('help.api.request')} codes={getServicesCode} />
</ApiExample>
</ApiSection>
{/* Service Object */}
<ApiSection id="service-object">
<ApiContent>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
{t('help.api.serviceObject')}
</h2>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.serviceObjectDescription')}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.attributes')}
</h3>
<AttributeTable
attributes={[
{ name: 'id', type: 'string', description: 'Unique identifier for the service.' },
{ name: 'name', type: 'string', description: 'The service name.' },
{ name: 'description', type: 'string', description: 'Service description.' },
{ name: 'duration', type: 'integer', description: 'Duration in minutes.' },
{ name: 'price', type: 'string', description: 'Price as a decimal string.' },
{ name: 'photos', type: 'array', description: 'URLs of service photos.' },
{ name: 'is_active', type: 'boolean', description: 'Whether the service is active.' },
]}
/>
</ApiContent>
<ApiExample>
<CodeBlock
title="THE SERVICE OBJECT"
language="json"
code={`{
"id": "svc_a1b2c3d4e5f6",
"name": "Haircut",
"description": "Professional haircut with wash and style",
"duration": 45,
"price": "35.00",
"photos": [
"https://cdn.smoothschedule.com/photos/haircut1.jpg"
],
"is_active": true
}`}
/>
</ApiExample>
</ApiSection>
{/* List Services */}
<ApiSection id="list-services">
<ApiContent>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0 flex items-center gap-2">
<span className="text-xs px-2 py-0.5 rounded bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 font-mono">
GET
</span>
{t('help.api.listServices')}
</h2>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.listServicesDescription')}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.requiredScope')}
</h3>
<p className="text-gray-600 dark:text-gray-300">
<code>services:read</code>
</p>
</ApiContent>
<ApiExample>
<CodeBlock
title={t('help.api.endpoint')}
language="http"
code="GET /v1/services/"
/>
<TabbedCodeBlock title={t('help.api.request')} codes={getServicesCode} />
<CodeBlock
title={t('help.api.response')}
language="json"
code={`[
{
"id": "svc_a1b2c3d4e5f6",
"name": "Haircut",
"description": "Professional haircut with wash and style",
"duration": 45,
"price": "35.00",
"is_active": true
},
{
"id": "svc_b2c3d4e5f6g7",
"name": "Color Treatment",
"description": "Full color treatment",
"duration": 120,
"price": "150.00",
"is_active": true
}
]`}
/>
</ApiExample>
</ApiSection>
{/* Check Availability */}
<ApiSection id="check-availability">
<ApiContent>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0 flex items-center gap-2">
<span className="text-xs px-2 py-0.5 rounded bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 font-mono">
GET
</span>
{t('help.api.checkAvailability')}
</h2>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.checkAvailabilityDescription')}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.parameters')}
</h3>
<AttributeTable
attributes={[
{ name: 'service_id', type: 'string', description: 'The service to check availability for.', required: true },
{ name: 'date', type: 'string', description: 'Start date in YYYY-MM-DD format.', required: true },
{ name: 'days', type: 'integer', description: 'Number of days to check (1-30). Default: 7' },
{ name: 'resource_id', type: 'string', description: 'Filter by specific resource.' },
]}
/>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.requiredScope')}
</h3>
<p className="text-gray-600 dark:text-gray-300">
<code>availability:read</code>
</p>
</ApiContent>
<ApiExample>
<CodeBlock
title={t('help.api.endpoint')}
language="http"
code="GET /v1/availability/"
/>
<TabbedCodeBlock title={t('help.api.request')} codes={getAvailabilityCode} />
<CodeBlock
title={t('help.api.response')}
language="json"
code={`{
"service": {
"id": "svc_a1b2c3d4e5f6",
"name": "Haircut",
"duration": 45
},
"date_range": {
"start": "2025-12-01",
"end": "2025-12-08"
},
"slots": [
{
"start_time": "2025-12-01T09:00:00Z",
"end_time": "2025-12-01T09:45:00Z",
"resource_id": "res_123",
"resource_name": "Jane Smith"
},
{
"start_time": "2025-12-01T10:00:00Z",
"end_time": "2025-12-01T10:45:00Z",
"resource_id": "res_123",
"resource_name": "Jane Smith"
}
]
}`}
/>
</ApiExample>
</ApiSection>
{/* Create Appointment */}
<ApiSection id="create-appointment">
<ApiContent>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0 flex items-center gap-2">
<span className="text-xs px-2 py-0.5 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 font-mono">
POST
</span>
{t('help.api.createAppointment')}
</h2>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.createAppointmentDescription')}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.parameters')}
</h3>
<AttributeTable
attributes={[
{ name: 'service_id', type: 'string', description: 'ID of the service being booked.', required: true },
{ name: 'resource_id', type: 'string', description: 'ID of the resource (staff/room).', required: true },
{ name: 'start_time', type: 'string', description: 'ISO 8601 datetime for the appointment start.', required: true },
{ name: 'customer_email', type: 'string', description: 'Customer email address.', required: true },
{ name: 'customer_name', type: 'string', description: 'Customer full name.', required: true },
{ name: 'customer_phone', type: 'string', description: 'Customer phone number.' },
{ name: 'notes', type: 'string', description: 'Additional notes for the appointment.' },
]}
/>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.requiredScope')}
</h3>
<p className="text-gray-600 dark:text-gray-300">
<code>bookings:write</code>
</p>
</ApiContent>
<ApiExample>
<CodeBlock
title={t('help.api.endpoint')}
language="http"
code="POST /v1/appointments/"
/>
<TabbedCodeBlock title={t('help.api.request')} codes={createAppointmentCode} />
<CodeBlock
title={t('help.api.response')}
language="json"
code={`{
"id": "apt_a1b2c3d4e5f6",
"service": {
"id": "svc_123",
"name": "Haircut"
},
"resource": {
"id": "res_456",
"name": "Jane Smith"
},
"customer": {
"id": "cust_789",
"name": "John Doe",
"email": "john@example.com"
},
"start_time": "2025-12-01T10:00:00Z",
"end_time": "2025-12-01T10:45:00Z",
"status": "scheduled",
"created_at": "2025-11-28T15:30:00Z"
}`}
/>
</ApiExample>
</ApiSection>
{/* Webhook Events */}
<ApiSection id="webhook-events">
<ApiContent>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
{t('help.api.webhookEvents')}
</h2>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.webhookEventsDescription')}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.eventTypes')}
</h3>
<AttributeTable
attributes={[
{ name: 'appointment.created', type: 'event', description: 'Fired when a new appointment is booked.' },
{ name: 'appointment.updated', type: 'event', description: 'Fired when an appointment is modified.' },
{ name: 'appointment.cancelled', type: 'event', description: 'Fired when an appointment is cancelled.' },
{ name: 'appointment.completed', type: 'event', description: 'Fired when an appointment is marked complete.' },
{ name: 'customer.created', type: 'event', description: 'Fired when a new customer is created.' },
{ name: 'customer.updated', type: 'event', description: 'Fired when customer info is updated.' },
]}
/>
</ApiContent>
<ApiExample>
<CodeBlock
title={t('help.api.webhookPayload')}
language="json"
code={`{
"id": "evt_a1b2c3d4e5f6",
"type": "appointment.created",
"created_at": "2025-11-28T15:30:00Z",
"data": {
"id": "apt_a1b2c3d4e5f6",
"service": {
"id": "svc_123",
"name": "Haircut"
},
"customer": {
"id": "cust_789",
"name": "John Doe"
},
"start_time": "2025-12-01T10:00:00Z",
"status": "scheduled"
}
}`}
/>
</ApiExample>
</ApiSection>
{/* Create Webhook */}
<ApiSection id="create-webhook">
<ApiContent>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0 flex items-center gap-2">
<span className="text-xs px-2 py-0.5 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 font-mono">
POST
</span>
{t('help.api.createWebhook')}
</h2>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.createWebhookDescription')}
<strong>{t('help.api.secretOnlyOnce')}</strong>
{t('help.api.secretOnlyOnceDescription')}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.parameters')}
</h3>
<AttributeTable
attributes={[
{ name: 'url', type: 'string', description: 'The HTTPS URL to receive webhook payloads.', required: true },
{ name: 'events', type: 'array', description: 'List of event types to subscribe to.', required: true },
{ name: 'description', type: 'string', description: 'Optional description for the webhook.' },
]}
/>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.requiredScope')}
</h3>
<p className="text-gray-600 dark:text-gray-300">
<code>webhooks:manage</code>
</p>
</ApiContent>
<ApiExample>
<CodeBlock
title={t('help.api.endpoint')}
language="http"
code="POST /v1/webhooks/"
/>
<TabbedCodeBlock title={t('help.api.request')} codes={createWebhookCode} />
<CodeBlock
title={t('help.api.response')}
language="json"
code={`{
"id": "whk_a1b2c3d4e5f6",
"url": "https://example.com/webhooks",
"events": [
"appointment.created",
"appointment.cancelled"
],
"secret": "${TEST_WEBHOOK_SECRET}",
"is_active": true,
"created_at": "2025-11-28T15:30:00Z"
}`}
/>
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
<p className="text-sm text-yellow-800 dark:text-yellow-200">
<strong>{t('help.api.saveYourSecret')}</strong> {t('help.api.saveYourSecretDescription')}
</p>
</div>
</ApiExample>
</ApiSection>
{/* Verify Signatures */}
<ApiSection id="verify-signatures">
<ApiContent>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
{t('help.api.verifySignatures')}
</h2>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.verifySignaturesDescription')}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.signatureFormat')}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{t('help.api.signatureFormatDescription')}
</p>
<pre className="bg-gray-100 dark:bg-gray-800 p-3 rounded-lg text-sm overflow-x-auto">
<code>&lt;timestamp&gt;.&lt;signature&gt;</code>
</pre>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('help.api.verificationSteps')}
</h3>
<ol className="text-gray-600 dark:text-gray-300 list-decimal list-inside space-y-2">
<li>{t('help.api.verificationStep1')}</li>
<li>{t('help.api.verificationStep2')}</li>
<li>{t('help.api.verificationStep3')}</li>
<li>{t('help.api.verificationStep4')}</li>
</ol>
</ApiContent>
<ApiExample>
<CodeBlock
title="SIGNATURE HEADER"
language="http"
code={`X-Webhook-Signature: 1732795200.a1b2c3d4e5f6...`}
/>
<TabbedCodeBlock title="VERIFICATION" codes={verifyWebhookCode} />
</ApiExample>
</ApiSection>
{/* Footer padding */}
<div className="h-24" />
</main>
</div>
</div>
</LanguageContext.Provider>
);
};
export default HelpApiDocs;