A comprehensive developer reference for integrating external services, APIs, and data persistence in Claude Interactive Artifacts
Interactive Artifacts operate as React components in a sandboxed iframe environment. Think of them as "serverless frontends" with the following characteristics:
If you're coming from traditional server-side development, adjust your thinking:
Traditional: Frontend ↔ Your Backend ↔ Database
Artifacts: React Component ↔ Cloud Services (APIs/Supabase/Webhooks)Browser Storage APIs are COMPLETELY BLOCKED:
// ❌ These will cause artifacts to FAIL
localStorage.setItem('data', value);
sessionStorage.setItem('data', value);
indexedDB.open('database');No Direct File System Access:
fetch() API for HTTP requestsuseState, useReducer)window.claude.completeONLY https://cdnjs.cloudflare.com is whitelisted due to Content Security Policy (CSP) restrictions. This is much more limiting than normal web development.
HTML Artifacts:
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.6.0/axios.min.js"></script>React Artifacts - Dynamic Loading:
const loadLibrary = async (libraryName, version, globalName) => {
return new Promise((resolve, reject) => {
if (window[globalName]) {
resolve(window[globalName]);
return;
}
const script = document.createElement('script');
script.src = `https://cdnjs.cloudflare.com/ajax/libs/${libraryName}/${version}/${libraryName}.min.js`;
script.onload = () => resolve(window[globalName]);
script.onerror = reject;
document.head.appendChild(script);
});
};
// Usage
const Chart = await loadLibrary('chart.js', '4.4.0', 'Chart');// ✅ Always use specific versions
https://cdnjs.cloudflare.com/ajax/libs/chart.js/4.4.0/chart.min.js
// ❌ Never use 'latest' - will break when updated
https://cdnjs.cloudflare.com/ajax/libs/chart.js/latest/chart.min.js// Option 1: Inline simple utilities
const utils = {
uuid: () => Date.now().toString(36) + Math.random().toString(36).substr(2),
debounce: (func, wait) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
};
// Option 2: Use available alternatives
// Instead of: react-query → use fetch with useState
// Instead of: styled-components → use Tailwind classes
// Instead of: react-router → use conditional renderingconst apiCall = async (endpoint, data, options = {}) => {
const defaultOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${options.apiKey || ''}`
},
body: JSON.stringify(data)
};
try {
const response = await fetch(endpoint, { ...defaultOptions, ...options });
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API call failed:', error);
throw error;
}
};const robustApiCall = async (url, data, retries = 3) => {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
signal: AbortSignal.timeout(10000) // 10s timeout
});
if (response.ok) return await response.json();
throw new Error(`HTTP ${response.status}`);
} catch (error) {
console.warn(`API attempt ${i + 1} failed:`, error.message);
if (i === retries - 1) throw error;
// Exponential backoff
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
}
}
};// Load Supabase from CDN
const loadSupabase = async () => {
if (window.supabase) return window.supabase;
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/supabase/2.38.0/supabase.min.js';
document.head.appendChild(script);
return new Promise(resolve => {
script.onload = () => resolve(window.supabase);
});
};
// Initialize client
const supabaseUrl = 'https://your-project.supabase.co';
const supabaseAnonKey = 'your-anon-key';
const { createClient } = await loadSupabase();
const supabase = createClient(supabaseUrl, supabaseAnonKey);// Create
const saveQuizResult = async (result) => {
const { data, error } = await supabase
.from('quiz_results')
.insert({
user_id: result.userId,
quiz_name: result.quizName,
score: result.score,
total_questions: result.totalQuestions,
answers: result.answers,
completed_at: new Date().toISOString()
});
if (error) throw error;
return data;
};
// Read
const getQuizResults = async (userId) => {
const { data, error } = await supabase
.from('quiz_results')
.select('*')
.eq('user_id', userId)
.order('completed_at', { ascending: false });
if (error) throw error;
return data;
};
// Update
const updateQuizResult = async (id, updates) => {
const { data, error } = await supabase
.from('quiz_results')
.update(updates)
.eq('id', id);
if (error) throw error;
return data;
};const subscribeToQuizResults = (userId, callback) => {
return supabase
.channel('quiz_results')
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'quiz_results',
filter: `user_id=eq.${userId}`
}, callback)
.subscribe();
};const downloadFile = (data, filename, type = 'application/json') => {
const blob = new Blob([data], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// Usage examples
const downloadQuizResults = (results) => {
// JSON download
downloadFile(
JSON.stringify(results, null, 2),
'quiz-results.json',
'application/json'
);
// CSV download
const csvData = convertToCSV(results);
downloadFile(csvData, 'quiz-results.csv', 'text/csv');
// PDF report (requires external API)
generatePDFReport(results).then(pdfBlob => {
downloadFile(pdfBlob, 'quiz-report.pdf', 'application/pdf');
});
};const convertToCSV = (data) => {
if (!data.length) return '';
const headers = Object.keys(data[0]);
const csvHeaders = headers.join(',');
const csvRows = data.map(row =>
headers.map(header => {
const value = row[header];
// Escape commas and quotes
return typeof value === 'string' && (value.includes(',') || value.includes('"'))
? `"${value.replace(/"/g, '""')}"`
: value;
}).join(',')
);
return [csvHeaders, ...csvRows].join('\n');
};Webhooks are HTTP POST endpoints that trigger external workflows. They enable integration with hundreds of services without building backend infrastructure.
const sendToZapier = async (webhookId, data) => {
const webhookUrl = `https://hooks.zapier.com/hooks/catch/${webhookId}/`;
try {
const response = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error(`Zapier webhook failed: ${response.status}`);
return true;
} catch (error) {
console.error('Zapier integration failed:', error);
return false;
}
};
// Quiz results to Google Sheets via Zapier
const saveToGoogleSheets = async (quizResults) => {
return await sendToZapier('XXXXX/YYYYY', {
timestamp: new Date().toISOString(),
user_name: quizResults.userName,
user_email: quizResults.userEmail,
quiz_name: quizResults.quizName,
score: quizResults.score,
total_questions: quizResults.totalQuestions,
percentage: Math.round((quizResults.score / quizResults.totalQuestions) * 100),
time_taken_minutes: quizResults.timeTaken,
answers_json: JSON.stringify(quizResults.answers)
});
};const sendToMake = async (webhookId, data) => {
const webhookUrl = `https://hook.eu1.make.com/${webhookId}`;
const response = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.ok;
};const sendToDiscord = async (webhookUrl, quizResults) => {
const embed = {
title: '🎯 Quiz Completed!',
description: `**${quizResults.userName}** completed **${quizResults.quizName}**`,
color: quizResults.percentage >= 80 ? 0x00ff00 : quizResults.percentage >= 60 ? 0xff9900 : 0xff0000,
fields: [
{ name: 'Score', value: `${quizResults.score}/${quizResults.totalQuestions}`, inline: true },
{ name: 'Percentage', value: `${quizResults.percentage}%`, inline: true },
{ name: 'Time Taken', value: `${quizResults.timeTaken} minutes`, inline: true }
],
timestamp: new Date().toISOString()
};
return await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ embeds: [embed] })
});
};const distributeResults = async (quizResults) => {
const integrations = [
() => saveToGoogleSheets(quizResults),
() => sendToDiscord(process.env.DISCORD_WEBHOOK, quizResults),
() => updateCRM(quizResults),
() => sendEmailNotification(quizResults)
];
const results = await Promise.allSettled(integrations.map(fn => fn()));
const failed = results
.map((result, index) => ({ result, index }))
.filter(({ result }) => result.status === 'rejected');
if (failed.length > 0) {
console.warn('Some integrations failed:', failed);
// Fallback to download
downloadFile(JSON.stringify(quizResults, null, 2), 'quiz-results-backup.json');
}
return results;
};const robustWebhook = async (url, data, maxRetries = 3) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
signal: AbortSignal.timeout(10000)
});
if (response.ok) return { success: true, attempt };
if (response.status >= 400 && response.status < 500) {
// Client error - don't retry
throw new Error(`Client error ${response.status}: ${response.statusText}`);
}
// Server error - retry
throw new Error(`Server error ${response.status}: ${response.statusText}`);
} catch (error) {
console.warn(`Webhook attempt ${attempt}/${maxRetries} failed:`, error.message);
if (attempt === maxRetries) {
return { success: false, error: error.message };
}
// Exponential backoff: 1s, 2s, 4s
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt - 1) * 1000));
}
}
};// Simple state for small apps
const [quizState, setQuizState] = useState({
currentQuestion: 0,
answers: [],
startTime: null,
isCompleted: false
});
// Complex state with useReducer
const quizReducer = (state, action) => {
switch (action.type) {
case 'START_QUIZ':
return { ...state, startTime: Date.now(), isStarted: true };
case 'ANSWER_QUESTION':
return {
...state,
answers: [...state.answers, action.answer],
currentQuestion: state.currentQuestion + 1
};
case 'COMPLETE_QUIZ':
return { ...state, isCompleted: true, endTime: Date.now() };
default:
return state;
}
};
const [quizState, dispatch] = useReducer(quizReducer, initialState);const QuizContext = createContext();
const QuizProvider = ({ children }) => {
const [state, dispatch] = useReducer(quizReducer, initialState);
const [apiStatus, setApiStatus] = useState('idle');
const submitResults = async (results) => {
setApiStatus('loading');
try {
await saveToSupabase(results);
await sendToZapier('webhook-id', results);
setApiStatus('success');
} catch (error) {
setApiStatus('error');
// Fallback to download
downloadFile(JSON.stringify(results, null, 2), 'quiz-backup.json');
}
};
return (
<QuizContext.Provider value={{ state, dispatch, submitResults, apiStatus }}>
{children}
</QuizContext.Provider>
);
};// ❌ Never expose sensitive keys in frontend
const OPENAI_API_KEY = 'sk-abcd1234...'; // Visible to all users!
// ✅ Use environment-specific configuration
const getConfig = (environment = 'production') => {
const configs = {
development: {
supabaseUrl: 'https://dev-project.supabase.co',
zapierWebhook: 'https://hooks.zapier.com/hooks/catch/DEV_ID/'
},
production: {
supabaseUrl: 'https://prod-project.supabase.co',
zapierWebhook: 'https://hooks.zapier.com/hooks/catch/PROD_ID/'
}
};
return configs[environment] || configs.production;
};
// ✅ Use proxy services for sensitive operations
const callOpenAIViaSupabase = async (prompt) => {
// Call Supabase Edge Function that handles OpenAI API securely
const { data } = await supabase.functions.invoke('openai-proxy', {
body: { prompt }
});
return data;
};const validateQuizInput = (input) => {
const errors = [];
if (!input.userName || input.userName.trim().length < 2) {
errors.push('Name must be at least 2 characters');
}
if (input.userEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.userEmail)) {
errors.push('Invalid email format');
}
if (!input.answers || !Array.isArray(input.answers)) {
errors.push('Answers must be provided');
}
return errors;
};const sanitizeUserInput = (input) => {
if (typeof input !== 'string') return input;
return input
.trim()
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') // Remove scripts
.replace(/javascript:/gi, '') // Remove javascript: protocols
.slice(0, 1000); // Limit length
};const QuizApp = () => {
const [quizState, setQuizState] = useState({
questions: [], // Embedded or loaded from Supabase
currentQuestion: 0,
answers: [],
startTime: null,
endTime: null,
userInfo: {}
});
const submitQuiz = async () => {
const results = {
...quizState.userInfo,
score: calculateScore(quizState.answers),
totalQuestions: quizState.questions.length,
timeTaken: (quizState.endTime - quizState.startTime) / 1000 / 60, // minutes
answers: quizState.answers
};
// Multiple persistence strategies
const strategies = [
() => saveToSupabase(results),
() => sendToZapier('webhook-id', results),
() => downloadFile(JSON.stringify(results, null, 2), 'quiz-results.json')
];
// Try each strategy, continue on failure
for (const strategy of strategies) {
try {
await strategy();
console.log('Results saved successfully');
break;
} catch (error) {
console.warn('Strategy failed, trying next:', error);
}
}
};
return (
<div className="quiz-container">
{/* Quiz UI components */}
</div>
);
};const DataDashboard = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
// Combine multiple data sources
const [quizResults, apiData, webhookStatus] = await Promise.allSettled([
supabase.from('quiz_results').select('*'),
fetch('https://api.external-service.com/stats').then(r => r.json()),
fetch('https://status.zapier.com/api/v2/summary.json').then(r => r.json())
]);
setData({
quizResults: quizResults.value?.data || [],
apiData: apiData.value || {},
webhookStatus: webhookStatus.value || {}
});
} catch (error) {
console.error('Failed to load dashboard data:', error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
return (
<div className="dashboard">
{loading ? <LoadingSpinner /> : <DataVisualization data={data} />}
</div>
);
};const FileProcessor = () => {
const [results, setResults] = useState([]);
const processFile = async (file) => {
try {
// Read uploaded file
const content = await window.fs.readFile(file.name, { encoding: 'utf8' });
// Process with external API
const processedData = await fetch('https://api.processing-service.com/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
}).then(r => r.json());
// Save results
await saveToSupabase(processedData);
// Trigger webhook for notifications
await sendToZapier('processing-complete', {
filename: file.name,
results: processedData,
timestamp: new Date().toISOString()
});
setResults(prev => [...prev, processedData]);
} catch (error) {
console.error('File processing failed:', error);
// Fallback: download original file with error log
downloadFile(
JSON.stringify({ error: error.message, filename: file.name }),
'processing-error.json'
);
}
};
return (
<div className="file-processor">
<input type="file" onChange={(e) => processFile(e.target.files[0])} />
<ResultsList results={results} />
</div>
);
};// Problem: Library not loading
const loadLibraryWithFallback = async (primary, fallback) => {
try {
return await loadLibrary(primary.name, primary.version, primary.global);
} catch (error) {
console.warn('Primary library failed, trying fallback:', error);
return await loadLibrary(fallback.name, fallback.version, fallback.global);
}
};
// Usage
const chart = await loadLibraryWithFallback(
{ name: 'chart.js', version: '4.4.0', global: 'Chart' },
{ name: 'chart.js', version: '3.9.1', global: 'Chart' }
);// Problem: CORS blocking API calls
// Solution: Use proxy services or CORS-enabled endpoints
const corsProxyCall = async (url, options) => {
// Use public CORS proxy (not for production)
const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`;
// Better: Use your own proxy service
const betterProxyUrl = `https://your-proxy.vercel.app/api/proxy?url=${encodeURIComponent(url)}`;
return fetch(betterProxyUrl, options);
};// Problem: Webhooks failing silently
const debugWebhook = async (url, data) => {
console.log('Webhook URL:', url);
console.log('Payload:', JSON.stringify(data, null, 2));
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
console.log('Response status:', response.status);
console.log('Response headers:', [...response.headers.entries()]);
const responseText = await response.text();
console.log('Response body:', responseText);
return response.ok;
} catch (error) {
console.error('Webhook debug error:', error);
return false;
}
};// Problem: State not updating correctly
const useDebugState = (initialState, label) => {
const [state, setState] = useState(initialState);
useEffect(() => {
console.log(`${label} state changed:`, state);
}, [state, label]);
const debugSetState = useCallback((newState) => {
console.log(`${label} state update:`, { from: state, to: newState });
setState(newState);
}, [state, label]);
return [state, debugSetState];
};// Debounce API calls
const useDebouncedApiCall = (apiFunction, delay = 500) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const debouncedCall = useCallback(
debounce(async (...args) => {
setLoading(true);
setError(null);
try {
const result = await apiFunction(...args);
return result;
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}, delay),
[apiFunction, delay]
);
return { call: debouncedCall, loading, error };
};
// Batch operations
const batchApiCalls = async (calls, batchSize = 3) => {
const results = [];
for (let i = 0; i < calls.length; i += batchSize) {
const batch = calls.slice(i, i + batchSize);
const batchResults = await Promise.allSettled(batch.map(call => call()));
results.push(...batchResults);
// Rate limiting delay
if (i + batchSize < calls.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return results;
};Interactive Artifacts provide a powerful platform for building data-driven applications with external service integration. Key takeaways:
This guide covers the fundamental patterns you'll need for most integration scenarios. The combination of React state management, external APIs, and webhook integrations enables building sophisticated applications without traditional backend infrastructure.
Last updated: July 2025 For questions or contributions, refer to the Claude Interactive Artifacts documentation.