Developer ReferenceWebhooks & Integrations

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

EventDescriptionBackend Action
checkout.session.completedUser completed checkoutCreate/update subscription, set tier
customer.subscription.createdNew subscription createdInitialize BrainBits quota
customer.subscription.updatedPlan changed (upgrade/downgrade)Update tier, adjust quota
customer.subscription.deletedSubscription cancelledDowngrade to Free tier
customer.subscription.trial_will_endTrial ending in 3 daysSend notification (future)

Invoice Events

EventDescriptionBackend Action
invoice.paidInvoice successfully paidRecord payment, reset monthly quota
invoice.payment_failedPayment failedFlag account, send notification
invoice.upcomingInvoice will be created soonPre-calculate usage

Token Purchase Events

EventDescriptionBackend Action
checkout.session.completed (one-time)Token block purchasedAdd BrainBits to balance

Webhook Security

The backend validates every webhook request:

  1. Read the raw request body.
  2. Extract the Stripe-Signature header.
  3. Verify using stripe.Webhook.construct_event() with STRIPE_WEBHOOK_SECRET.
  4. 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:

HeaderDescription
X-Goog-Channel-IDChannel identifier
X-Goog-Resource-IDResource being watched
X-Goog-Resource-Statesync, update, add, remove, trash, untrash
X-Goog-ChangedComma-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

MethodPathDescription
POST/api/drive/webhook/registerRegister watch channel
DELETE/api/drive/webhook/unregisterStop watching
GET/api/drive/webhook/statusChannel registration status
POST/api/drive/force-syncForce 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:

MethodPathDescription
GET/api/presence/{tenant_id}List online users for tenant
GET/api/presence/{tenant_id}/editingList 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:

  1. The push response includes the conflicting entity in the conflicts array.
  2. The user resolves the conflict in the desktop UI (choose local, choose cloud, or manual merge).
  3. The resolution is submitted via POST /api/desktop/sync/conflicts/{conflict_id}/resolve.

Sync Management

MethodPathDescription
POST/api/desktop/sync/startStart background sync worker
POST/api/desktop/sync/stopStop sync worker
POST/api/desktop/sync/nowTrigger immediate sync
POST/api/desktop/sync/force-pushForce push (overwrites cloud)
POST/api/desktop/sync/force-pullForce pull (overwrites local)
GET/api/desktop/sync/statusSync worker status and last result
GET/api/desktop/sync/queueView pending sync operations
GET/api/desktop/sync/conflictsList 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