Skip to content

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.

EventTrigger
file.hotFile confirmed and stored in hot cache
file.syncedFile successfully synced to Walrus
file.coldFile evicted from hot cache (Walrus only)
file.errorFile processing failed
file.deletedFile soft-deleted (moved to trash)
file.rehydratedCold file rehydrated back to hot cache
EventTrigger
vault.createdA new vault was created
vault.updatedA vault was renamed or updated
vault.deletedA vault was deleted
vault.member_addedA member was granted access to a shared vault
vault.member_removedA 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.

const webhook = await opentusk.webhooks.create({
url: 'https://example.com/opentusk-webhook',
events: ['file.synced', 'file.error'],
});
// Save the secret — it's only shown once
console.log(webhook.secret);

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"
}
}

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.

Use SSE whenUse webhooks when
The client is a user-facing app (web, mobile, CLI) that’s already authenticatedThe receiver is a long-running server-side service
You don’t want to operate a public-internet endpointYou need delivery guarantees and retries — webhooks are persisted and retried
You want events only while the user is connectedYou want events even when the client is offline
You need browser-friendly streamingYou 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.

Terminal window
curl -N -H "Authorization: Bearer otk_your_key" \
https://api.opentusk.ai/api/events/stream

The connection receives the same events as webhooks — file status changes, vault lifecycle events, and membership changes. Events are scoped to the authenticated user.

Events are delivered in standard SSE format. A connected event fires immediately when the stream opens, then events stream as they happen:

event: connected
data: {}
event: file.synced
data: {"fileId":"file-uuid","fileName":"report.pdf","vaultId":"vault-uuid","status":"synced","sizeBytes":204800,"walrusBlobId":"blob-id-hash","timestamp":"..."}
event: vault.created
data: {"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 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`);
});

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 handler
app.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);
});

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 OK

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'})`);
}
// Update events or disable
await opentusk.webhooks.update(webhook.id, {
events: ['file.synced'],
active: false,
});
// Delete
await opentusk.webhooks.delete(webhook.id);