Webhooks
Tensoras webhooks deliver real-time event notifications to your application when things happen in the platform — documents are ingested, batch jobs complete, fine-tuning finishes, and more. Instead of polling the API, your server receives HTTP POST requests with event payloads as they occur.
Overview
When you register a webhook, Tensoras sends an HTTP POST request to your specified URL whenever a subscribed event occurs. Each delivery includes a JSON payload describing the event and an HMAC-SHA256 signature so you can verify authenticity.
Key features:
- 14 event types covering ingestion, documents, knowledge bases, batches, fine-tuning, data sources, evaluations, and billing
- HMAC-SHA256 signatures on every delivery for security
- Automatic retries with exponential backoff for failed deliveries
- Delivery logs to inspect past webhook attempts and responses
Setup
Creating a Webhook via the API
from tensoras import Tensoras
client = Tensoras(api_key="tns_your_key_here")
webhook = client.webhooks.create(
url="https://your-app.example.com/webhooks/tensoras",
events=["ingestion.completed", "ingestion.failed", "batch.completed"],
secret="whsec_your_signing_secret",
)
print(webhook.id) # wh_abc123import Tensoras from "tensoras";
const client = new Tensoras({ apiKey: "tns_your_key_here" });
const webhook = await client.webhooks.create({
url: "https://your-app.example.com/webhooks/tensoras",
events: ["ingestion.completed", "ingestion.failed", "batch.completed"],
secret: "whsec_your_signing_secret",
});
console.log(webhook.id); // wh_abc123Creating a Webhook via the Console
- Navigate to Console > Settings > Webhooks.
- Click Create Webhook.
- Enter your endpoint URL and select the events you want to subscribe to.
- Copy the generated signing secret and store it securely.
Authentication
Every webhook delivery includes an X-Tensoras-Signature header containing an HMAC-SHA256 signature of the request body. Always verify this signature before processing the payload.
Signature Headers
| Header | Description |
|---|---|
X-Tensoras-Signature | sha256=<hex-encoded HMAC-SHA256> of the raw request body |
X-Tensoras-Timestamp | Unix timestamp when the delivery was sent |
X-Tensoras-Event | The event type (e.g., ingestion.completed) |
X-Tensoras-Delivery | Unique delivery ID for idempotency |
Verifying Signatures in Python
import hashlib
import hmac
def verify_webhook(payload_body: bytes, signature_header: str, secret: str) -> bool:
"""Verify the HMAC-SHA256 signature of a Tensoras webhook."""
if not signature_header.startswith("sha256="):
return False
expected_signature = signature_header.removeprefix("sha256=")
computed = hmac.new(
secret.encode("utf-8"),
payload_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(computed, expected_signature)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_signing_secret"
@app.post("/webhooks/tensoras")
def handle_webhook():
signature = request.headers.get("X-Tensoras-Signature", "")
if not verify_webhook(request.data, signature, WEBHOOK_SECRET):
abort(401)
event = request.json
event_type = event["type"]
if event_type == "ingestion.completed":
job_id = event["data"]["job_id"]
print(f"Ingestion job {job_id} completed")
elif event_type == "batch.completed":
batch_id = event["data"]["batch_id"]
print(f"Batch {batch_id} completed")
return "", 200Verifying Signatures in Node.js
import crypto from "node:crypto";
import express from "express";
const app = express();
const WEBHOOK_SECRET = "whsec_your_signing_secret";
function verifyWebhook(payloadBody, signatureHeader, secret) {
if (!signatureHeader?.startsWith("sha256=")) return false;
const expectedSignature = signatureHeader.slice("sha256=".length);
const computed = crypto
.createHmac("sha256", secret)
.update(payloadBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(computed, "hex"),
Buffer.from(expectedSignature, "hex")
);
}
app.post(
"/webhooks/tensoras",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.headers["x-tensoras-signature"] ?? "";
if (!verifyWebhook(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body.toString());
switch (event.type) {
case "ingestion.completed":
console.log(`Ingestion job ${event.data.job_id} completed`);
break;
case "batch.completed":
console.log(`Batch ${event.data.batch_id} completed`);
break;
}
res.status(200).send("OK");
}
);
app.listen(3000);Event Catalog
Tensoras supports the following webhook event types. Subscribe to specific events when creating a webhook, or omit the events field to receive all events.
Ingestion Events
| Event | Description | Payload Fields |
|---|---|---|
ingestion.completed | Document ingestion job finished successfully | job_id, knowledge_base_id, status, docs_added, docs_failed, chunks_created |
ingestion.failed | Document ingestion job failed | job_id, knowledge_base_id, status, error, docs_added, docs_failed, chunks_created |
Document Events
| Event | Description | Payload Fields |
|---|---|---|
document.created | New document added to a knowledge base | document_id, knowledge_base_id, name, status |
document.deleted | Document removed from a knowledge base | document_id, knowledge_base_id, name, status |
Knowledge Base Events
| Event | Description | Payload Fields |
|---|---|---|
knowledge_base.created | New knowledge base created | knowledge_base_id, name |
knowledge_base.deleted | Knowledge base deleted | knowledge_base_id, name |
Batch Events
| Event | Description | Payload Fields |
|---|---|---|
batch.completed | Batch processing job completed | batch_id, status, input_file_id, output_file_id, error_file_id, request_counts |
batch.failed | Batch processing job failed | batch_id, status, input_file_id, output_file_id, error_file_id, request_counts |
The request_counts object contains: total, completed, failed.
Fine-Tuning Events
| Event | Description | Payload Fields |
|---|---|---|
fine_tuning.completed | Fine-tuning job completed successfully | job_id, model, status, result_model |
fine_tuning.failed | Fine-tuning job failed | job_id, model, status, error |
Data Source Events
| Event | Description | Payload Fields |
|---|---|---|
data_source.sync_completed | Data source sync finished successfully | data_source_id, knowledge_base_id, status |
data_source.sync_failed | Data source sync failed | data_source_id, knowledge_base_id, status, error |
Evaluation Events
| Event | Description | Payload Fields |
|---|---|---|
evaluation.completed | Evaluation run completed | evaluation_id, name, knowledge_base_id, status, total_samples, completed_samples |
Billing Events
| Event | Description | Payload Fields |
|---|---|---|
billing.low_balance | Organization credit balance dropped below the configured threshold | org_id, current_balance_usd, threshold_usd |
Payload Format
Every webhook delivery uses a common envelope structure:
{
"id": "del_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "ingestion.completed",
"created_at": 1708700000,
"data": {
"job_id": "job_abc123",
"knowledge_base_id": "kb_def456",
"status": "completed",
"docs_added": 15,
"docs_failed": 0,
"chunks_created": 342
}
}| Field | Type | Description |
|---|---|---|
id | string | Unique delivery ID (UUID). Use this for idempotency. |
type | string | The event type from the catalog above. |
created_at | integer | Unix timestamp when the event was generated. |
data | object | Event-specific payload (see catalog for fields). |
Example Payloads
ingestion.completed
{
"id": "del_f47ac10b-58cc-4372-a567-0e02b2c3d479",
"type": "ingestion.completed",
"created_at": 1708700000,
"data": {
"job_id": "job_abc123",
"knowledge_base_id": "kb_def456",
"status": "completed",
"docs_added": 15,
"docs_failed": 0,
"chunks_created": 342,
"error": null
}
}batch.completed
{
"id": "del_c9bf9e57-1685-4c89-bafb-ff5af830be8a",
"type": "batch.completed",
"created_at": 1708700500,
"data": {
"batch_id": "batch_xyz789",
"status": "completed",
"input_file_id": "file-input-001",
"output_file_id": "file-output-001",
"error_file_id": null,
"request_counts": {
"total": 500,
"completed": 495,
"failed": 5
}
}
}fine_tuning.completed
{
"id": "del_6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"type": "fine_tuning.completed",
"created_at": 1708701000,
"data": {
"job_id": "ft_abc123",
"model": "llama-3.3-70b",
"status": "completed",
"result_model": "ft:llama-3.3-70b:my-org:custom-suffix",
"error": null
}
}billing.low_balance
{
"id": "del_550e8400-e29b-41d4-a716-446655440000",
"type": "billing.low_balance",
"created_at": 1708701500,
"data": {
"org_id": "org_abc123",
"current_balance_usd": 2.45,
"threshold_usd": 5.0
}
}Retry Policy
If your endpoint does not return a 2xx status code, Tensoras retries the delivery with exponential backoff:
| Attempt | Delay After Failure |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 12 hours |
After 5 failed retries (6 total attempts), the delivery is marked as failed and no further retries are attempted. You can inspect failed deliveries in the Console under Settings > Webhooks > Delivery Log.
Best Practices
Respond Quickly
Your webhook endpoint should return a 200 response within 30 seconds. If processing the event takes longer, acknowledge the delivery immediately and process the event asynchronously:
@app.post("/webhooks/tensoras")
def handle_webhook():
# Verify signature first
signature = request.headers.get("X-Tensoras-Signature", "")
if not verify_webhook(request.data, signature, WEBHOOK_SECRET):
abort(401)
event = request.json
# Enqueue for async processing -- respond immediately
task_queue.enqueue(process_event, event)
return "", 200Implement Idempotency
Use the id field from the delivery envelope to deduplicate events. The same event may be delivered more than once if your endpoint times out or returns a non-2xx status and the delivery is retried.
def process_event(event: dict):
delivery_id = event["id"]
# Check if already processed
if redis_client.sismember("processed_webhooks", delivery_id):
return # Already handled
# Process the event
handle_event(event)
# Mark as processed
redis_client.sadd("processed_webhooks", delivery_id)Use HTTPS
Always use an HTTPS endpoint URL. Tensoras will not deliver webhooks to plain HTTP endpoints in production.
Monitor Delivery Health
Check your webhook delivery logs periodically in the Console. A high failure rate may indicate that your endpoint is down, returning errors, or too slow.
Related
- Billing — the
billing.low_balanceevent and spending controls - Batches API — batch processing that triggers
batch.completed/batch.failed - Knowledge Bases — KB operations that trigger document and ingestion events
- Fine-Tuning — fine-tuning jobs that trigger
fine_tuning.completed/fine_tuning.failed