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:
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
2xxwithin 10 seconds - Must verify the signature before processing
- Should return
200 OKeven for events you don't handle (to prevent unnecessary retries) - For idempotency, use
delivery_idto deduplicate on your side