GuidesWebhooks

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_abc123
import 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_abc123

Creating a Webhook via the Console

  1. Navigate to Console > Settings > Webhooks.
  2. Click Create Webhook.
  3. Enter your endpoint URL and select the events you want to subscribe to.
  4. 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

HeaderDescription
X-Tensoras-Signaturesha256=<hex-encoded HMAC-SHA256> of the raw request body
X-Tensoras-TimestampUnix timestamp when the delivery was sent
X-Tensoras-EventThe event type (e.g., ingestion.completed)
X-Tensoras-DeliveryUnique 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 "", 200

Verifying 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

EventDescriptionPayload Fields
ingestion.completedDocument ingestion job finished successfullyjob_id, knowledge_base_id, status, docs_added, docs_failed, chunks_created
ingestion.failedDocument ingestion job failedjob_id, knowledge_base_id, status, error, docs_added, docs_failed, chunks_created

Document Events

EventDescriptionPayload Fields
document.createdNew document added to a knowledge basedocument_id, knowledge_base_id, name, status
document.deletedDocument removed from a knowledge basedocument_id, knowledge_base_id, name, status

Knowledge Base Events

EventDescriptionPayload Fields
knowledge_base.createdNew knowledge base createdknowledge_base_id, name
knowledge_base.deletedKnowledge base deletedknowledge_base_id, name

Batch Events

EventDescriptionPayload Fields
batch.completedBatch processing job completedbatch_id, status, input_file_id, output_file_id, error_file_id, request_counts
batch.failedBatch processing job failedbatch_id, status, input_file_id, output_file_id, error_file_id, request_counts

The request_counts object contains: total, completed, failed.

Fine-Tuning Events

EventDescriptionPayload Fields
fine_tuning.completedFine-tuning job completed successfullyjob_id, model, status, result_model
fine_tuning.failedFine-tuning job failedjob_id, model, status, error

Data Source Events

EventDescriptionPayload Fields
data_source.sync_completedData source sync finished successfullydata_source_id, knowledge_base_id, status
data_source.sync_failedData source sync faileddata_source_id, knowledge_base_id, status, error

Evaluation Events

EventDescriptionPayload Fields
evaluation.completedEvaluation run completedevaluation_id, name, knowledge_base_id, status, total_samples, completed_samples

Billing Events

EventDescriptionPayload Fields
billing.low_balanceOrganization credit balance dropped below the configured thresholdorg_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
  }
}
FieldTypeDescription
idstringUnique delivery ID (UUID). Use this for idempotency.
typestringThe event type from the catalog above.
created_atintegerUnix timestamp when the event was generated.
dataobjectEvent-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:

AttemptDelay After Failure
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry12 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 "", 200

Implement 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.

  • Billing — the billing.low_balance event 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