Webhooks
Webhooks let you receive HTTP callbacks when file statuses change or shared vault membership changes. Instead of polling, your server gets notified in real time.
Events
Section titled “Events”File events
Section titled “File events”| Event | Trigger |
|---|---|
file.hot | File confirmed and stored in hot cache |
file.synced | File successfully synced to Walrus |
file.cold | File evicted from hot cache (Walrus only) |
file.error | File processing failed |
file.deleted | File soft-deleted (moved to trash) |
file.rehydrated | Cold file rehydrated back to hot cache |
Vault events
Section titled “Vault events”| Event | Trigger |
|---|---|
vault.created | A new vault was created |
vault.updated | A vault was renamed or updated |
vault.deleted | A vault was deleted |
vault.member_added | A member was granted access to a shared vault |
vault.member_removed | A member’s access was revoked from a shared vault |
Vault member events are delivered to all active members of the shared vault who have webhook endpoints registered.
Creating a webhook
Section titled “Creating a webhook”const webhook = await opentusk.webhooks.create({ url: 'https://example.com/opentusk-webhook', events: ['file.synced', 'file.error'],});
// Save the secret — it's only shown onceconsole.log(webhook.secret);resp = requests.post( "https://api.opentusk.ai/api/webhooks", headers=headers, json={ "url": "https://example.com/opentusk-webhook", "events": ["file.synced", "file.error"], },)webhook = resp.json()# Save webhook["secret"] — only shown oncecurl -X POST https://api.opentusk.ai/api/webhooks \ -H "Authorization: Bearer otk_your_key" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/opentusk-webhook", "events": ["file.synced", "file.error"] }'Payload format
Section titled “Payload format”OpenTusk sends a POST request with a JSON body:
{ "event": "file.synced", "timestamp": "2025-01-15T10:30:00.000Z", "data": { "fileId": "file-uuid", "fileName": "report.pdf", "vaultId": "vault-uuid", "status": "synced", "sizeBytes": 204800, "walrusBlobId": "blob-id-hash" }}For file.error events, the payload includes an error field:
{ "event": "file.error", "timestamp": "2025-01-15T10:30:00.000Z", "data": { "fileId": "file-uuid", "fileName": "report.pdf", "vaultId": "vault-uuid", "status": "error", "sizeBytes": 204800, "error": "Walrus sync failed: insufficient WAL balance" }}For vault.member_added events:
{ "event": "vault.member_added", "timestamp": "2025-03-16T12:00:00.000Z", "data": { "vaultId": "vault-uuid", "vaultName": "Team Project", "suiAddress": "0x1234...abcd", "role": "member", "grantedBy": "user-uuid" }}For vault.member_removed events:
{ "event": "vault.member_removed", "timestamp": "2025-03-16T12:30:00.000Z", "data": { "vaultId": "vault-uuid", "vaultName": "Team Project", "suiAddress": "0x1234...abcd", "revokedBy": "user-uuid" }}For file.deleted events:
{ "event": "file.deleted", "timestamp": "2025-03-16T14:00:00.000Z", "data": { "fileId": "file-uuid", "fileName": "report.pdf", "vaultId": "vault-uuid", "status": "hot", "sizeBytes": 204800, "walrusBlobId": "blob-id-hash" }}For file.rehydrated events:
{ "event": "file.rehydrated", "timestamp": "2025-03-16T15:00:00.000Z", "data": { "fileId": "file-uuid", "fileName": "report.pdf", "vaultId": "vault-uuid", "status": "hot", "sizeBytes": 204800, "walrusBlobId": "blob-id-hash" }}For vault lifecycle events (vault.created, vault.updated, vault.deleted):
{ "event": "vault.created", "timestamp": "2025-03-16T16:00:00.000Z", "data": { "vaultId": "vault-uuid", "vaultName": "My New Vault" }}Real-time events (SSE)
Section titled “Real-time events (SSE)”As an alternative to webhooks, you can receive events in real time using Server-Sent Events (SSE) at GET /api/events/stream. This is useful for frontend applications, dashboards, or any short-lived client that wants a persistent event stream without setting up a public webhook endpoint.
SSE vs webhooks — which to use
Section titled “SSE vs webhooks — which to use”| Use SSE when | Use webhooks when |
|---|---|
| The client is a user-facing app (web, mobile, CLI) that’s already authenticated | The receiver is a long-running server-side service |
| You don’t want to operate a public-internet endpoint | You need delivery guarantees and retries — webhooks are persisted and retried |
| You want events only while the user is connected | You want events even when the client is offline |
| You need browser-friendly streaming | You need cross-origin delivery to arbitrary URLs |
Webhooks survive client downtime and retry on failure; SSE connections are ephemeral — if the client disconnects, missed events are not replayed.
Connecting
Section titled “Connecting”curl -N -H "Authorization: Bearer otk_your_key" \ https://api.opentusk.ai/api/events/streamThe connection receives the same events as webhooks — file status changes, vault lifecycle events, and membership changes. Events are scoped to the authenticated user.
Event format
Section titled “Event format”Events are delivered in standard SSE format. A connected event fires immediately when the stream opens, then events stream as they happen:
event: connecteddata: {}
event: file.synceddata: {"fileId":"file-uuid","fileName":"report.pdf","vaultId":"vault-uuid","status":"synced","sizeBytes":204800,"walrusBlobId":"blob-id-hash","timestamp":"..."}
event: vault.createddata: {"vaultId":"vault-uuid","vaultName":"My New Vault","timestamp":"..."}Heartbeat comments (:keepalive) are sent every 30 seconds to keep the connection alive through proxies and load balancers.
Browser usage
Section titled “Browser usage”Browser EventSource does not support custom headers natively. For an authenticated SSE connection from the browser, use a fetch-based polyfill (e.g. @microsoft/fetch-event-source) or proxy the stream through your backend:
import { fetchEventSource } from '@microsoft/fetch-event-source';
await fetchEventSource('https://api.opentusk.ai/api/events/stream', { headers: { Authorization: 'Bearer otk_your_key' }, onmessage(ev) { if (ev.event === 'file.synced') { const data = JSON.parse(ev.data); console.log(`${data.fileName} synced to Walrus`); } },});For Node.js, the EventSource from the eventsource package accepts headers:
import EventSource from 'eventsource';
const es = new EventSource('https://api.opentusk.ai/api/events/stream', { headers: { Authorization: 'Bearer otk_your_key' },});
es.addEventListener('file.synced', (e) => { const data = JSON.parse(e.data); console.log(`${data.fileName} synced`);});Signature verification
Section titled “Signature verification”Every webhook request includes an X-OpenTusk-Signature header. Verify it using HMAC-SHA-256 with your webhook secret:
import { createHmac } from 'crypto';
function verifyWebhook(body: string, signature: string, secret: string): boolean { const expected = createHmac('sha256', secret).update(body).digest('hex'); return signature === expected;}
// In your webhook handlerapp.post('/opentusk-webhook', (req, res) => { const signature = req.headers['x-opentusk-signature'] as string; if (!verifyWebhook(JSON.stringify(req.body), signature, WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); }
const { event, data } = req.body; console.log(`${event}: ${data.fileName} → ${data.status}`); res.sendStatus(200);});import hmacimport hashlib
def verify_webhook(body: bytes, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected)
# In your webhook handler (Flask example)@app.route("/opentusk-webhook", methods=["POST"])def handle_webhook(): signature = request.headers.get("X-OpenTusk-Signature", "") if not verify_webhook(request.data, signature, WEBHOOK_SECRET): return "Invalid signature", 401
payload = request.json print(f"{payload['event']}: {payload['data']['fileName']}") return "", 200Testing webhooks
Section titled “Testing webhooks”Send a test delivery to verify your endpoint:
const delivery = await opentusk.webhooks.test(webhook.id);console.log(delivery.statusCode); // 200 if your server responded OKcurl -X POST https://api.opentusk.ai/api/webhooks/{webhookId}/test \ -H "Authorization: Bearer otk_your_key"Viewing deliveries
Section titled “Viewing deliveries”Check the delivery history for a webhook:
const deliveries = await opentusk.webhooks.listDeliveries(webhook.id, { limit: 10,});
for (const d of deliveries) { console.log(`${d.event} → ${d.statusCode} (${d.success ? 'ok' : 'failed'})`);}Managing webhooks
Section titled “Managing webhooks”// Update events or disableawait opentusk.webhooks.update(webhook.id, { events: ['file.synced'], active: false,});
// Deleteawait opentusk.webhooks.delete(webhook.id);