import { useStore } from '@nanostores/react'
import { useState } from 'react'
import { $apiKeys, createAPIKey, $deleteAPIKey, $creating } from '../stores/apiKeys'
import { $webhooks, createWebhook, updateWebhook, $deleteWebhook, testWebhook, fetchWebhookDeliveries } from '../stores/webhooks'
import { $billing } from '../stores/billing'
import { addToast } from '../stores/app'
import { EmptyState, APIPageSkeleton, PageHeader } from '../components/shared'
import { Icons } from '../components/shared/Icons'
import { Button, Input, Modal, UsageIndicator } from '../components/ui'
import type { Webhook, WebhookDelivery, WebhookEvent } from '../types'
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
interface QueryParam {
name: string
description: string
default?: string
options?: string[] // For select dropdowns
}
interface Endpoint {
method: HttpMethod
path: string
description: string
queryParams?: QueryParam[]
requestBody?: string
responseExample: string
}
const endpoints: Endpoint[] = [
{
method: 'GET',
path: '/api/v1/posts',
description: 'List published posts with pagination',
queryParams: [
{ name: 'limit', description: 'Results per page (max 100)', default: '20' },
{ name: 'offset', description: 'Skip N results', default: '0' },
{ name: 'tag', description: 'Filter by tag' },
{ name: 'include', description: 'Include extra fields', options: ['', 'content'] },
],
responseExample: `{
"posts": [
{
"id": "uuid",
"slug": "hello-world",
"title": "Hello World",
"description": "My first post",
"tags": ["intro"],
"date": "2024-01-15",
"draft": false
}
],
"total": 42,
"limit": 20,
"offset": 0
}`,
},
{
method: 'POST',
path: '/api/v1/posts',
description: 'Create a new post',
requestBody: `{
"title": "My New Post",
"slug": "my-new-post",
"content": "# Markdown content",
"description": "Optional description",
"tags": ["tutorial"],
"draft": false
}`,
responseExample: `{
"id": "uuid",
"slug": "my-new-post",
"title": "My New Post",
...
}`,
},
{
method: 'GET',
path: '/api/v1/posts/{slug}',
description: 'Get a single post by slug (includes content)',
responseExample: `{
"id": "uuid",
"slug": "hello-world",
"title": "Hello World",
"description": "My first post",
"content": {
"markdown": "# Full markdown...",
"html": "
Full markdown...
"
},
"tags": ["intro"],
"date": "2024-01-15"
}`,
},
{
method: 'PUT',
path: '/api/v1/posts/{slug}',
description: 'Update an existing post',
requestBody: `{
"title": "Updated Title",
"content": "# Updated content"
}`,
responseExample: `{
"id": "uuid",
"slug": "hello-world",
"title": "Updated Title",
...
}`,
},
{
method: 'DELETE',
path: '/api/v1/posts/{slug}',
description: 'Delete a post permanently',
responseExample: `204 No Content`,
},
{
method: 'GET',
path: '/api/v1/settings',
description: 'Get public site configuration',
responseExample: `{
"site_name": "My Blog",
"site_description": "A developer blog",
"author_name": "Jane Doe",
"author_bio": "Software engineer",
"twitter_handle": "janedoe",
"accent_color": "#10b981"
}`,
},
]
const methodColors: Record = {
GET: 'bg-success/15 text-success',
POST: 'bg-blue-500/15 text-blue-500',
PUT: 'bg-warning/15 text-warning',
DELETE: 'bg-danger/15 text-danger',
}
const webhookEvents: { value: WebhookEvent; label: string }[] = [
{ value: 'post.published', label: 'Post Published' },
{ value: 'post.updated', label: 'Post Updated' },
{ value: 'post.deleted', label: 'Post Deleted' },
]
function EndpointCard({ endpoint, baseUrl, apiKey }: { endpoint: Endpoint; baseUrl: string; apiKey?: string }) {
const [expanded, setExpanded] = useState(false)
const [slugInput, setSlugInput] = useState('hello-world')
const [requestBody, setRequestBody] = useState(endpoint.requestBody || '')
const [queryParams, setQueryParams] = useState>(() => {
const initial: Record = {}
endpoint.queryParams?.forEach(p => {
initial[p.name] = p.default || ''
})
return initial
})
const [response, setResponse] = useState<{ status: number; body: string; time: number } | null>(null)
const [loading, setLoading] = useState(false)
const basePath = endpoint.path.replace('{slug}', slugInput)
const queryString = Object.entries(queryParams)
.filter(([_, v]) => v !== '')
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join('&')
const actualPath = queryString ? `${basePath}?${queryString}` : basePath
const fullUrl = `${baseUrl}${actualPath}`
const curlExample = endpoint.requestBody
? `curl -X ${endpoint.method} "${fullUrl}" \\
-H "Authorization: Bearer ${apiKey || 'YOUR_API_KEY'}" \\
-H "Content-Type: application/json" \\
-d '${endpoint.requestBody.replace(/\n\s*/g, ' ')}'`
: `curl -X ${endpoint.method} "${fullUrl}" \\
-H "Authorization: Bearer ${apiKey || 'YOUR_API_KEY'}"`
const copyCode = (code: string) => {
navigator.clipboard.writeText(code)
addToast('Copied to clipboard', 'success')
}
const updateQueryParam = (name: string, value: string) => {
setQueryParams(prev => ({ ...prev, [name]: value }))
}
const sendRequest = async () => {
setLoading(true)
setResponse(null)
const start = Date.now()
try {
const headers: Record = {
'Content-Type': 'application/json',
}
if (apiKey) {
headers['Authorization'] = `Bearer ${apiKey}`
}
const res = await fetch(actualPath, {
method: endpoint.method,
headers,
body: endpoint.requestBody ? requestBody : undefined,
})
const time = Date.now() - start
let body: string
try {
const json = await res.json()
body = JSON.stringify(json, null, 2)
} catch {
body = res.status === 204 ? '(No Content)' : await res.text()
}
setResponse({ status: res.status, body, time })
} catch (err) {
setResponse({ status: 0, body: `Error: ${err instanceof Error ? err.message : 'Request failed'}`, time: Date.now() - start })
} finally {
setLoading(false)
}
}
return (
{expanded && (
{endpoint.description}
{endpoint.path.includes('{slug}') && (
)}
{endpoint.queryParams && endpoint.queryParams.length > 0 && (
Query Parameters
{endpoint.queryParams.map(param => (
{param.options ? (
) : (
updateQueryParam(param.name, v)}
className="max-w-32 text-sm"
placeholder={param.default || ''}
/>
)}
{param.description}
))}
)}
{endpoint.requestBody && (
)}
{response && (
Response
= 200 && response.status < 300 ? 'bg-success/15 text-success' : response.status >= 400 ? 'bg-danger/15 text-danger' : 'bg-warning/15 text-warning'}`}>
{response.status || 'Error'}
{response.time}ms
{response.body}
)}
Example Response
{endpoint.responseExample}
cURL
{curlExample}
)}
)
}
function WebhookCard({ webhook, onEdit, onDelete, onTest }: {
webhook: Webhook
onEdit: () => void
onDelete: () => void
onTest: () => void
}) {
const [showLogs, setShowLogs] = useState(false)
const [deliveries, setDeliveries] = useState([])
const [loadingLogs, setLoadingLogs] = useState(false)
const loadDeliveries = async () => {
setLoadingLogs(true)
try {
const data = await fetchWebhookDeliveries(webhook.id)
setDeliveries(data)
setShowLogs(true)
} catch {
addToast('Failed to load delivery logs', 'error')
} finally {
setLoadingLogs(false)
}
}
return (
{webhook.name}
{!webhook.enabled && (disabled)}
{webhook.url}
{webhook.events.map(event => (
{event}
))}
{showLogs && (
Recent Deliveries
{loadingLogs ? (
Loading...
) : deliveries.length === 0 ? (
No deliveries yet
) : (
{deliveries.slice(0, 10).map(d => (
{d.event}
•
{d.response_code || d.status}
{new Date(d.created_at).toLocaleString()}
))}
)}
)}
)
}
export default function APIPage() {
const { data: keys } = useStore($apiKeys)
const { data: webhooks } = useStore($webhooks)
const { data: billing } = useStore($billing)
const creating = useStore($creating)
const deleteKey = useStore($deleteAPIKey)
const deleteWebhookMutation = useStore($deleteWebhook)
const [showCreateKey, setShowCreateKey] = useState(false)
const [newKeyName, setNewKeyName] = useState('')
const [createdKey, setCreatedKey] = useState(null)
const [deleteKeyModal, setDeleteKeyModal] = useState(null)
const [showWebhookModal, setShowWebhookModal] = useState(false)
const [editingWebhook, setEditingWebhook] = useState(null)
const [webhookForm, setWebhookForm] = useState({ name: '', url: '', events: [] as WebhookEvent[], secret: '', enabled: true })
const [deleteWebhookModal, setDeleteWebhookModal] = useState(null)
const [savingWebhook, setSavingWebhook] = useState(false)
const handleCreateKey = async () => {
if (!newKeyName.trim()) return
try {
const result = await createAPIKey(newKeyName)
setCreatedKey(result.key)
setNewKeyName('')
setShowCreateKey(false)
addToast('API key created', 'success')
} catch {
addToast('Failed to create API key', 'error')
}
}
const handleDeleteKey = async () => {
if (!deleteKeyModal) return
try {
await deleteKey.mutate(deleteKeyModal)
addToast('API key deleted', 'success')
setDeleteKeyModal(null)
} catch {
addToast('Failed to delete API key', 'error')
}
}
const copyKey = (key: string) => {
navigator.clipboard.writeText(key)
addToast('Copied to clipboard', 'success')
}
const formatDate = (dateStr: string | null) => {
if (!dateStr) return 'Never'
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
}
const openWebhookModal = (webhook?: Webhook) => {
if (webhook) {
setEditingWebhook(webhook)
setWebhookForm({ name: webhook.name, url: webhook.url, events: webhook.events, secret: webhook.secret || '', enabled: webhook.enabled })
} else {
setEditingWebhook(null)
setWebhookForm({ name: '', url: '', events: [], secret: '', enabled: true })
}
setShowWebhookModal(true)
}
const handleSaveWebhook = async () => {
if (!webhookForm.name || !webhookForm.url || webhookForm.events.length === 0) {
addToast('Name, URL, and at least one event are required', 'error')
return
}
setSavingWebhook(true)
try {
if (editingWebhook) {
await updateWebhook(editingWebhook.id, webhookForm)
addToast('Webhook updated', 'success')
} else {
await createWebhook(webhookForm)
addToast('Webhook created', 'success')
}
setShowWebhookModal(false)
} catch {
addToast('Failed to save webhook', 'error')
} finally {
setSavingWebhook(false)
}
}
const handleDeleteWebhook = async () => {
if (!deleteWebhookModal) return
try {
await deleteWebhookMutation.mutate(deleteWebhookModal)
addToast('Webhook deleted', 'success')
setDeleteWebhookModal(null)
} catch {
addToast('Failed to delete webhook', 'error')
}
}
const handleTestWebhook = async (id: string) => {
try {
await testWebhook(id)
addToast('Test webhook sent', 'success')
} catch {
addToast('Failed to send test webhook', 'error')
}
}
const toggleEvent = (event: WebhookEvent) => {
setWebhookForm(f => ({
...f,
events: f.events.includes(event) ? f.events.filter(e => e !== event) : [...f.events, event]
}))
}
if (!keys) return
const baseUrl = window.location.origin
const firstKey = keys.length > 0 ? keys[0].key : undefined
return (
{createdKey && (
API Key Created
Copy this key now. You won't be able to see it again.
{createdKey}
)}
{/* API Keys */}
Your API Keys
Use these keys to authenticate API requests
{keys.length === 0 ? (
setShowCreateKey(true)}>Create Key}
/>
) : (
keys.map((key, i) => (
0 ? 'border-t border-border' : ''}`}>
{key.name}
{key.key.slice(0, 8)}...{key.key.slice(-4)}
•
Created {formatDate(key.created_at)}
•
Last used {formatDate(key.last_used_at)}
))
)}
{/* Webhooks */}
Webhooks
Get notified when posts are published, updated, or deleted
{billing && (
)}
{webhooks && webhooks.length > 0 ? (
webhooks.map(webhook => (
openWebhookModal(webhook)}
onDelete={() => setDeleteWebhookModal(webhook.id)}
onTest={() => handleTestWebhook(webhook.id)}
/>
))
) : (
No webhooks configured
)}
{billing && billing.usage.webhooks >= billing.tiers[billing.current_tier].max_webhooks ? (
Webhook limit reached.
) : (
)}
{/* API Reference */}
API Reference
Base URL: {baseUrl}
{endpoints.map((endpoint) => (
))}
{/* Authentication */}
Authentication
How to authenticate your requests
All API requests require a Bearer token in the Authorization header:
Authorization: Bearer {firstKey ? `${firstKey.slice(0, 8)}...` : 'YOUR_API_KEY'}
Keep API keys secure and never expose them in client-side code.
{/* Create Key Modal */}
setShowCreateKey(false)} title="Create API Key">
{/* Delete Key Modal */}
setDeleteKeyModal(null)} title="Delete API Key">
Are you sure? Applications using this key will stop working.
{/* Webhook Modal */}
setShowWebhookModal(false)} title={editingWebhook ? 'Edit Webhook' : 'Add Webhook'}>
{/* Delete Webhook Modal */}
setDeleteWebhookModal(null)} title="Delete Webhook">
Are you sure you want to delete this webhook?
)
}