Webhooks
Webhooks let you receive HTTP callbacks when file statuses change. Instead of polling, your server gets notified in real time.
Events
Section titled “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 |
Creating a webhook
Section titled “Creating a webhook”const webhook = await tusky.webhooks.create({ url: 'https://example.com/tusky-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/tusky-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 tdp_your_key" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/tusky-webhook", "events": ["file.synced", "file.error"] }'Payload format
Section titled “Payload format”Tusky 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" }}Signature verification
Section titled “Signature verification”Every webhook request includes an X-Tusky-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('/tusky-webhook', (req, res) => { const signature = req.headers['x-tusky-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("/tusky-webhook", methods=["POST"])def handle_webhook(): signature = request.headers.get("X-Tusky-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 tusky.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 tdp_your_key"Viewing deliveries
Section titled “Viewing deliveries”Check the delivery history for a webhook:
const deliveries = await tusky.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 tusky.webhooks.update(webhook.id, { events: ['file.synced'], active: false,});
// Deleteawait tusky.webhooks.delete(webhook.id);