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 = { 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_'; 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> = { 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 {code}; } 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 ( {result.map((segment, i) => ( {segment.text} ))} {lineIndex < lines.length - 1 && '\n'} ); })} ); }; // ============================================================================= // 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 (
{title && (
{title} {LANGUAGES[language]?.label || language}
)}
          {highlightSyntax(code, language)}
        
); }; 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 = ['curl', 'javascript', 'python', 'go', 'java', 'csharp', 'php', 'ruby', 'perl']; return (
{title && (
{title}
)}
{tabs.map((lang) => ( ))}
          {highlightSyntax(codes[activeLanguage], activeLanguage)}
        
); }; // ============================================================================= // 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 ( {method} ); }; const Sidebar: React.FC<{ activeSection: string; onSectionClick: (id: string) => void; t: (key: string) => string; }> = ({ activeSection, onSectionClick, t, }) => { const [expandedSections, setExpandedSections] = useState(['business', 'services', 'appointments']); const toggleSection = (id: string) => { setExpandedSections(prev => prev.includes(id) ? prev.filter(s => s !== id) : [...prev, id] ); }; return ( ); }; // ============================================================================= // API SECTION COMPONENT (Stripe-style split pane) // ============================================================================= interface ApiSectionProps { id: string; children: React.ReactNode; } const ApiSection: React.FC = ({ id, children }) => { return (
{children}
); }; const ApiContent: React.FC<{ children: React.ReactNode }> = ({ children }) => (
{children}
); const ApiExample: React.FC<{ children: React.ReactNode }> = ({ children }) => (
{children}
); // ============================================================================= // ATTRIBUTE TABLE // ============================================================================= interface Attribute { name: string; type: string; description: string; required?: boolean; } const AttributeTable: React.FC<{ attributes: Attribute[] }> = ({ attributes }) => (
{attributes.map(attr => (
{attr.name} {attr.type} {attr.required && ( required )}

{attr.description}

))}
); // ============================================================================= // MAIN COMPONENT // ============================================================================= const HelpApiDocs: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); const [activeSection, setActiveSection] = useState('introduction'); const [activeLanguage, setActiveLanguage] = useState('curl'); const [selectedTokenId, setSelectedTokenId] = useState(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 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: . echo -n "." | \\ 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 (
{/* Header */}

{t('help.api.title')}

{/* Test Token Selector */} {testTokens && testTokens.length > 0 && (
)} {t('help.api.interactiveExplorer')}
{/* No Test Tokens Banner */} {!tokensLoading && (!testTokens || testTokens.length === 0) && (

{t('help.api.noTestTokensFound')}

)} {/* Token Loading Error */} {tokensError && (

{t('help.api.errorLoadingTokens')}

{t('help.api.errorLoadingTokensMessage')}

)}
{/* Sidebar */} {/* Main Content */}
{/* Introduction */}

{t('help.api.introduction')}

{t('help.api.introDescription')}

{t('help.api.introTestMode')}

{t('help.api.baseUrl')}

{t('help.api.baseUrlDescription')}

{t('help.api.sandboxMode')} {t('help.api.sandboxModeDescription')}

{/* Authentication */}

{t('help.api.authentication')}

{t('help.api.authDescription')}

{t('help.api.authBearer')}

{t('help.api.authWarning')}

{t('help.api.apiKeyFormat')}

  • ss_test_* — {t('help.api.testKey')}
  • ss_live_* — {t('help.api.liveKey')}

{t('help.api.keepKeysSecret')} {t('help.api.keepKeysSecretDescription')}

{/* Errors */}

{t('help.api.errors')}

{t('help.api.errorsDescription')}

{t('help.api.httpStatusCodes')}

{/* Rate Limits */}

{t('help.api.rateLimits')}

{t('help.api.rateLimitsDescription')}

{t('help.api.limits')}

  • 1,000 {t('help.api.requestsPerHour')}
  • 100 {t('help.api.requestsPerMinute')}

{t('help.api.rateLimitHeaders')}

{t('help.api.rateLimitHeadersDescription')}

{/* Business Object */}

{t('help.api.businessObject')}

{t('help.api.businessObjectDescription')}

{t('help.api.attributes')}

{/* Retrieve Business */}

GET {t('help.api.retrieveBusiness')}

{t('help.api.retrieveBusinessDescription')}

{t('help.api.requiredScope')}

business:read

{/* Service Object */}

{t('help.api.serviceObject')}

{t('help.api.serviceObjectDescription')}

{t('help.api.attributes')}

{/* List Services */}

GET {t('help.api.listServices')}

{t('help.api.listServicesDescription')}

{t('help.api.requiredScope')}

services:read

{/* Check Availability */}

GET {t('help.api.checkAvailability')}

{t('help.api.checkAvailabilityDescription')}

{t('help.api.parameters')}

{t('help.api.requiredScope')}

availability:read

{/* Create Appointment */}

POST {t('help.api.createAppointment')}

{t('help.api.createAppointmentDescription')}

{t('help.api.parameters')}

{t('help.api.requiredScope')}

bookings:write

{/* Webhook Events */}

{t('help.api.webhookEvents')}

{t('help.api.webhookEventsDescription')}

{t('help.api.eventTypes')}

{/* Create Webhook */}

POST {t('help.api.createWebhook')}

{t('help.api.createWebhookDescription')} {t('help.api.secretOnlyOnce')} {t('help.api.secretOnlyOnceDescription')}

{t('help.api.parameters')}

{t('help.api.requiredScope')}

webhooks:manage

{t('help.api.saveYourSecret')} {t('help.api.saveYourSecretDescription')}

{/* Verify Signatures */}

{t('help.api.verifySignatures')}

{t('help.api.verifySignaturesDescription')}

{t('help.api.signatureFormat')}

{t('help.api.signatureFormatDescription')}

                  <timestamp>.<signature>
                

{t('help.api.verificationSteps')}

  1. {t('help.api.verificationStep1')}
  2. {t('help.api.verificationStep2')}
  3. {t('help.api.verificationStep3')}
  4. {t('help.api.verificationStep4')}
{/* Footer padding */}
); }; export default HelpApiDocs;