Skip to content

Webhooks

Webhooks let you receive real-time notifications when translation jobs reach a terminal state, instead of polling the API.


Setup

Webhook URL and secret are configured per API key. Contact support or use the Dashboard to configure your webhook endpoint. (A self-service CRUD endpoint is planned.)


Signing & Verification

Every webhook request includes two headers:

Header Value
X-Falara-Signature sha256=<HMAC-SHA256 hex digest>
X-Falara-Timestamp Unix timestamp (integer seconds)
User-Agent Falara-Webhooks/1.0

The signed message is:

{timestamp}.{raw_request_body}

Verify the timestamp to prevent replay attacks (reject requests older than 5 minutes).

Verification Examples

import hmac, hashlib, time

def verify_webhook(payload: bytes, signature: str, timestamp: str, secret: str) -> bool:
    # Check timestamp freshness (max 5 minutes)
    if abs(time.time() - int(timestamp)) > 300:
        return False

    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.{payload.decode()}".encode(),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(f"sha256={expected}", signature)
const crypto = require("crypto");

function verifyWebhook(payload, signature, timestamp, secret) {
  if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${payload}`)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(`sha256=${expected}`),
    Buffer.from(signature)
  );
}
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "math"
    "strconv"
    "time"
)

func verifyWebhook(payload []byte, signature, timestamp, secret string) bool {
    ts, err := strconv.ParseInt(timestamp, 10, 64)
    if err != nil {
        return false
    }
    if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
        return false
    }
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(fmt.Sprintf("%s.%s", timestamp, payload)))
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}

Payload Envelope

All events share the same top-level structure:

{
  "delivery_id": "uuid",
  "event": "<event_name>",
  "event_version": 1,
  "timestamp": "2026-03-06T10:05:00+00:00",
  "data": { ... }
}

Events

job.completed

Fired when a job reaches completed or completed_with_blocks.

{
  "delivery_id": "uuid",
  "event": "job.completed",
  "event_version": 1,
  "timestamp": "2026-03-06T10:05:00+00:00",
  "data": {
    "job_id": "550e8400-e29b-41d4-a716-446655440000",
    "batch_id": null,
    "source_lang": "de",
    "target_lang": "en",
    "status": "completed",
    "has_delivery_notes": false,
    "result_url": "https://falara.io/v1/jobs/550e8400-.../result",
    "download_url": "https://falara.io/v1/jobs/550e8400-.../download"
  }
}

job.failed

Fired when a job reaches failed or dead.

{
  "delivery_id": "uuid",
  "event": "job.failed",
  "event_version": 1,
  "timestamp": "2026-03-06T10:05:00+00:00",
  "data": {
    "job_id": "550e8400-e29b-41d4-a716-446655440000",
    "batch_id": null,
    "source_lang": "de",
    "target_lang": "en",
    "status": "failed",
    "has_delivery_notes": false,
    "result_url": "https://falara.io/v1/jobs/550e8400-.../result",
    "download_url": "https://falara.io/v1/jobs/550e8400-.../download"
  }
}

job.needs_review

Fired when QA score stays below the minimum threshold after all correction loops.

{
  "delivery_id": "uuid",
  "event": "job.needs_review",
  "event_version": 1,
  "timestamp": "2026-03-06T10:05:00+00:00",
  "data": {
    "job_id": "550e8400-e29b-41d4-a716-446655440000",
    "batch_id": null,
    "source_lang": "de",
    "target_lang": "en",
    "status": "needs_review",
    "has_delivery_notes": true,
    "result_url": "https://falara.io/v1/jobs/550e8400-.../result",
    "download_url": "https://falara.io/v1/jobs/550e8400-.../download"
  }
}

batch.completed

Fired when all jobs in a batch have reached a terminal status.

{
  "delivery_id": "uuid",
  "event": "batch.completed",
  "event_version": 1,
  "timestamp": "2026-03-06T10:10:00+00:00",
  "data": {
    "batch_id": "b1c2d3e4-f5a6-7890-b1c2-d3e4f5a67890",
    "total_jobs": 4,
    "completed": 3,
    "failed": 0,
    "needs_review": 1,
    "download_url": "https://falara.io/v1/jobs/batch/b1c2d3e4-.../download"
  }
}
Field Type Description
total_jobs int Total jobs in the batch
completed int Jobs with completed or completed_with_blocks
failed int Jobs with failed or dead
needs_review int Jobs with needs_review

Status → Event Mapping

Job Status Webhook Event
completed job.completed
completed_with_blocks job.completed
failed job.failed
dead job.failed
needs_review job.needs_review
queued, processing, etc. — (no event)

Delivery

Deduplication: Each (job_id, event) pair is delivered at most once (24-hour window).

Retry schedule: On delivery failure, Falara retries up to 5 times:

Attempt Delay
1 30 seconds
2 2 minutes
3 10 minutes
4 30 minutes
5 2 hours

Permanent failures (no retry): HTTP 400, 401, 403, 404, 410, 422.

Crash recovery: In-flight webhook deliveries are automatically re-enqueued if a worker restarts.


Your endpoint requirements

  • Must respond with 2xx within 10 seconds
  • Must verify the signature before processing
  • Should return 200 OK even for events you don't handle (to prevent unnecessary retries)
  • For idempotency, use delivery_id to deduplicate on your side