Webhooks and Integrations
StudioBrain supports several real-time communication channels: Stripe webhooks for billing events, Google Drive push notifications for cloud storage sync, WebSocket events for live collaboration, and a desktop sync protocol for offline-capable workflows.
Stripe Webhooks
Stripe sends webhook events to POST /api/billing/webhook for subscription lifecycle management. The backend validates each event using the STRIPE_WEBHOOK_SECRET before processing.
Subscription Events
| Event | Description | Backend Action |
|---|---|---|
checkout.session.completed | User completed checkout | Create/update subscription, set tier |
customer.subscription.created | New subscription created | Initialize BrainBits quota |
customer.subscription.updated | Plan changed (upgrade/downgrade) | Update tier, adjust quota |
customer.subscription.deleted | Subscription cancelled | Downgrade to Free tier |
customer.subscription.trial_will_end | Trial ending in 3 days | Send notification (future) |
Invoice Events
| Event | Description | Backend Action |
|---|---|---|
invoice.paid | Invoice successfully paid | Record payment, reset monthly quota |
invoice.payment_failed | Payment failed | Flag account, send notification |
invoice.upcoming | Invoice will be created soon | Pre-calculate usage |
Token Purchase Events
| Event | Description | Backend Action |
|---|---|---|
checkout.session.completed (one-time) | Token block purchased | Add BrainBits to balance |
Webhook Security
The backend validates every webhook request:
- Read the raw request body.
- Extract the
Stripe-Signatureheader. - Verify using
stripe.Webhook.construct_event()withSTRIPE_WEBHOOK_SECRET. - Reject requests that fail signature verification.
@router.post("/webhook")
async def stripe_webhook(request: Request):
payload = await request.body()
sig_header = request.headers.get("stripe-signature")
event = stripe.Webhook.construct_event(payload, sig_header, WEBHOOK_SECRET)
# Process event...Idempotency
Stripe may deliver the same event multiple times. The backend uses the event ID for idempotent processing — duplicate events are acknowledged but not re-processed.
Google Drive Webhooks
When a tenant connects Google Drive storage (Indie+ tier), StudioBrain registers a push notification channel to receive real-time change notifications.
Registration
POST /api/drive/webhook/register
Authorization: Bearer {token}This creates a Google Drive watch channel that sends change notifications to POST /api/drive/webhook. The channel has a configurable expiration (default: 7 days) and auto-renews.
Incoming Notifications
Google sends a POST to the webhook URL with headers:
| Header | Description |
|---|---|
X-Goog-Channel-ID | Channel identifier |
X-Goog-Resource-ID | Resource being watched |
X-Goog-Resource-State | sync, update, add, remove, trash, untrash |
X-Goog-Changed | Comma-separated list of change types |
The backend processes these notifications to detect file changes in the linked Drive folder and triggers a sync operation.
Management
| Method | Path | Description |
|---|---|---|
| POST | /api/drive/webhook/register | Register watch channel |
| DELETE | /api/drive/webhook/unregister | Stop watching |
| GET | /api/drive/webhook/status | Channel registration status |
| POST | /api/drive/force-sync | Force immediate Drive sync |
WebSocket Events
StudioBrain provides a WebSocket endpoint for real-time updates. This is used for live collaboration (seeing other users’ edits), sync notifications, and system events.
Connection
const ws = new WebSocket('ws://host:8201/api/ws?token={jwt_token}');Or connect with the token in the first message:
const ws = new WebSocket('ws://host:8201/api/ws');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'auth', token: jwtToken }));
};The WebSocket requires a valid JWT. Unauthenticated connections are rejected.
Event Catalog
All events are JSON objects with a type field and event-specific payload.
entity_updated
Fired when an entity is created, updated, or deleted.
{
"type": "entity_updated",
"entity_type": "character",
"entity_id": "rex_marshall",
"action": "update",
"updated_fields": ["age", "personality_traits"],
"updated_by": "user_456",
"timestamp": "2026-02-24T12:00:00Z"
}action values: create, update, delete, restore.
sync_completed
Fired when a markdown-to-database sync finishes.
{
"type": "sync_completed",
"entities_synced": 42,
"errors": 0,
"duration_ms": 1250,
"timestamp": "2026-02-24T12:00:00Z"
}presence_update
Fired when a user comes online, goes offline, or starts/stops editing an entity.
{
"type": "presence_update",
"user_id": "user_456",
"user_name": "Jane",
"status": "editing",
"entity_type": "character",
"entity_id": "rex_marshall",
"timestamp": "2026-02-24T12:00:00Z"
}status values: online, offline, editing, idle.
brainbits_deducted
Fired when BrainBits are consumed for an AI operation.
{
"type": "brainbits_deducted",
"amount": 5,
"remaining": 495,
"operation": "chat_completion",
"timestamp": "2026-02-24T12:00:00Z"
}asset_processed
Fired when the asset queue finishes processing an asset (AI analysis, thumbnail generation).
{
"type": "asset_processed",
"asset_id": "asset_789",
"entity_type": "character",
"entity_id": "rex_marshall",
"processing_type": "ai_analysis",
"status": "completed",
"timestamp": "2026-02-24T12:00:00Z"
}Presence API
The REST presence endpoints complement the WebSocket:
| Method | Path | Description |
|---|---|---|
| GET | /api/presence/{tenant_id} | List online users for tenant |
| GET | /api/presence/{tenant_id}/editing | List who is editing what |
Frontend Hook
The frontend provides useRealtimeUpdates for subscribing to WebSocket events:
import { useRealtimeUpdates } from '@/hooks/useRealtimeUpdates';
function MyComponent() {
useRealtimeUpdates({
onEntityUpdated: (event) => {
// Refresh data when another user edits
refetchEntity();
},
onPresenceUpdate: (event) => {
// Show who else is editing
setActiveEditors(event);
},
});
}Redis PubSub Fan-Out
In cloud deployments with multiple backend instances, WebSocket events are distributed via Redis PubSub. The redis_pubsub service subscribes to tenant-scoped channels and fans out events to all connected WebSocket clients for that tenant.
Channel naming: studiobrain:tenant:{tenant_id}:events
Desktop Sync Protocol
The desktop app (Tauri 2.0) synchronizes local files with the cloud using a push/pull protocol.
Push (Local to Cloud)
The desktop client sends local changes to the cloud:
POST /api/desktop/sync/push
Authorization: Bearer {token}
Content-Type: application/json
{
"entities": [
{
"entity_type": "character",
"entity_id": "rex_marshall",
"version": 5,
"fields": { ... },
"markdown_body": "...",
"checksum": "sha256:abc123..."
}
],
"client_timestamp": "2026-02-24T12:00:00Z"
}Response:
{
"accepted": ["character/rex_marshall"],
"rejected": [],
"conflicts": []
}Pull (Cloud to Local)
The desktop client fetches changes since its last sync:
GET /api/desktop/sync/pull?since=2026-02-24T11:00:00Z
Authorization: Bearer {token}Response:
{
"entities": [
{
"entity_type": "location",
"entity_id": "downtown_plaza",
"version": 3,
"fields": { ... },
"markdown_body": "...",
"updated_by": "user_789",
"updated_at": "2026-02-24T11:30:00Z"
}
],
"has_more": false,
"server_timestamp": "2026-02-24T12:00:00Z"
}Conflict Detection
Conflicts occur when both the local and cloud versions of an entity have changed since the last sync. The backend detects this by comparing version numbers and checksums.
When a conflict is detected:
- The push response includes the conflicting entity in the
conflictsarray. - The user resolves the conflict in the desktop UI (choose local, choose cloud, or manual merge).
- The resolution is submitted via
POST /api/desktop/sync/conflicts/{conflict_id}/resolve.
Sync Management
| Method | Path | Description |
|---|---|---|
| POST | /api/desktop/sync/start | Start background sync worker |
| POST | /api/desktop/sync/stop | Stop sync worker |
| POST | /api/desktop/sync/now | Trigger immediate sync |
| POST | /api/desktop/sync/force-push | Force push (overwrites cloud) |
| POST | /api/desktop/sync/force-pull | Force pull (overwrites local) |
| GET | /api/desktop/sync/status | Sync worker status and last result |
| GET | /api/desktop/sync/queue | View pending sync operations |
| GET | /api/desktop/sync/conflicts | List unresolved conflicts |
Integration Patterns
Headless REST API Usage
StudioBrain’s backend can be used headlessly via the REST API for automation, CI/CD pipelines, or custom tools:
# Authenticate
TOKEN=$(curl -s -X POST http://localhost:8201/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"secret"}' \
| jq -r '.access_token')
# List all characters
curl -s http://localhost:8201/api/entity/character \
-H "Authorization: Bearer $TOKEN" | jq '.entities[].name'
# Create an entity
curl -s -X POST http://localhost:8201/api/entity/character \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"entity_id": "new_character",
"name": "New Character",
"fields": {"age": "25", "gender": "female"},
"markdown_body": "# New Character\n\nDescription here."
}'
# Trigger full sync
curl -s -X POST http://localhost:8201/api/sync/refresh-all \
-H "Authorization: Bearer $TOKEN"API Key Authentication
For automated tools, generate an API key:
# Create an API key
curl -s -X POST http://localhost:8201/api/api-keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "CI Pipeline"}'
# Use the API key
curl -s http://localhost:8201/api/entity/character \
-H "Authorization: Bearer {api_key}"Webhook Testing
For local development, use a tool like ngrok or localtunnel to expose your local backend for webhook delivery:
# Expose local backend
ngrok http 8201
# Configure Stripe webhook endpoint to ngrok URL
# Configure Google Drive webhook to ngrok URL