Skip to content

Webhooks

Receive real-time notifications when translation jobs complete, fail, or require review.


Configuration

Manage your webhook endpoint via the API. Each API key has its own webhook configuration.

GET /v1/webhooks/config

Get the current webhook configuration for the authenticated API key.

Authentication

Header: X-API-Key: your_api_key

Status: 200 OK

Response (200)

{
  "webhook_url": "https://example.com/hooks/falara",
  "webhook_secret_masked": "whsec_****abcd",
  "active": true,
  "updated_at": "2026-03-10T09:00:00+00:00"
}

Returns null fields and "active": false if no webhook is configured:

{
  "webhook_url": null,
  "webhook_secret_masked": null,
  "active": false,
  "updated_at": null
}

Examples

curl -X GET https://app.falara.io/v1/webhooks/config \
  -H "X-API-Key: $FALARA_API_KEY"
import requests

response = requests.get(
    "https://app.falara.io/v1/webhooks/config",
    headers={"X-API-Key": FALARA_API_KEY},
)
config = response.json()
print(f"Active: {config['active']}, URL: {config['webhook_url']}")
const response = await fetch("https://app.falara.io/v1/webhooks/config", {
  headers: { "X-API-Key": FALARA_API_KEY },
});
const config = await response.json();
console.log(`Active: ${config.active}, URL: ${config.webhook_url}`);

Errors

Status Description
401 Invalid or missing API key.

PUT /v1/webhooks/config

Create or update the webhook configuration for the authenticated API key.

Authentication

Header: X-API-Key: your_api_key

Content-Type: application/json Status: 200 OK

Request Body

Field Type Required Description
webhook_url string yes HTTPS URL to receive events.
webhook_secret string yes Secret used for HMAC-SHA256 signing.
{
  "webhook_url": "https://example.com/hooks/falara",
  "webhook_secret": "whsec_your_secret_here"
}

Warning

The webhook_secret must be at least 16 characters. Use a cryptographically random string.

Response (200)

{
  "webhook_url": "https://example.com/hooks/falara",
  "webhook_secret_masked": "whsec_****here",
  "active": true,
  "updated_at": "2026-03-22T14:00:00+00:00"
}

Examples

curl -X PUT https://app.falara.io/v1/webhooks/config \
  -H "X-API-Key: $FALARA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://example.com/hooks/falara",
    "webhook_secret": "whsec_your_secret_here"
  }'
import requests

response = requests.put(
    "https://app.falara.io/v1/webhooks/config",
    headers={"X-API-Key": FALARA_API_KEY},
    json={
        "webhook_url": "https://example.com/hooks/falara",
        "webhook_secret": "whsec_your_secret_here",
    },
)
print(response.json())
const response = await fetch("https://app.falara.io/v1/webhooks/config", {
  method: "PUT",
  headers: {
    "X-API-Key": FALARA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    webhook_url: "https://example.com/hooks/falara",
    webhook_secret: "whsec_your_secret_here",
  }),
});
console.log(await response.json());

Errors

Status Description
400 URL not HTTPS, or secret too short.
401 Invalid or missing API key.
422 Invalid webhook URL (e.g. resolves to internal network).

DELETE /v1/webhooks/config

Disable and remove the webhook configuration for the authenticated API key. Events will no longer be delivered.

Authentication

Header: X-API-Key: your_api_key

Status: 204 No Content

Examples

curl -X DELETE https://app.falara.io/v1/webhooks/config \
  -H "X-API-Key: $FALARA_API_KEY"
import requests

response = requests.delete(
    "https://app.falara.io/v1/webhooks/config",
    headers={"X-API-Key": FALARA_API_KEY},
)
assert response.status_code == 204
const response = await fetch("https://app.falara.io/v1/webhooks/config", {
  method: "DELETE",
  headers: { "X-API-Key": FALARA_API_KEY },
});
console.log(response.status); // 204

Errors

Status Description
401 Invalid or missing API key.

Signing & Verification

Every webhook delivery is signed with your webhook_secret using HMAC-SHA256. Two headers are included with each request:

Header Description
X-Falara-Signature sha256={hex_digest} -- the HMAC-SHA256 signature.
X-Falara-Timestamp Unix timestamp (seconds) when the payload was signed.

The signed message is constructed as:

{timestamp}.{json_body}

Verification Examples

import hashlib
import hmac
import time

def verify_webhook(body: bytes, signature_header: str, timestamp_header: str, secret: str) -> bool:
    """Verify the Falara webhook signature."""
    # 1. Reject stale timestamps (>5 minutes old)
    timestamp = int(timestamp_header)
    if abs(time.time() - timestamp) > 300:
        return False

    # 2. Reconstruct the signed message
    message = f"{timestamp}.{body.decode('utf-8')}".encode("utf-8")

    # 3. Compute expected signature
    expected = hmac.new(
        secret.encode("utf-8"), message, hashlib.sha256
    ).hexdigest()

    # 4. Compare (constant-time)
    received = signature_header.removeprefix("sha256=")
    return hmac.compare_digest(expected, received)
const crypto = require("crypto");

function verifyWebhook(body, signatureHeader, timestampHeader, secret) {
  // 1. Reject stale timestamps (>5 minutes old)
  const timestamp = parseInt(timestampHeader, 10);
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  // 2. Reconstruct the signed message
  const message = `${timestamp}.${body}`;

  // 3. Compute expected signature
  const expected = crypto
    .createHmac("sha256", secret)
    .update(message, "utf8")
    .digest("hex");

  // 4. Compare (constant-time)
  const received = signatureHeader.replace("sha256=", "");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(received)
  );
}
package webhook

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "math"
    "strconv"
    "time"
)

func VerifyWebhook(body []byte, signatureHeader, timestampHeader, secret string) bool {
    // 1. Reject stale timestamps (>5 minutes old)
    ts, err := strconv.ParseInt(timestampHeader, 10, 64)
    if err != nil {
        return false
    }
    if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
        return false
    }

    // 2. Reconstruct the signed message
    message := fmt.Sprintf("%d.%s", ts, string(body))

    // 3. Compute expected signature
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(message))
    expected := hex.EncodeToString(mac.Sum(nil))

    // 4. Compare (constant-time)
    received := signatureHeader[len("sha256="):]
    return hmac.Equal([]byte(expected), []byte(received))
}

Replay protection

Always validate the timestamp. Reject deliveries where X-Falara-Timestamp is more than 5 minutes old to prevent replay attacks.


Payload Envelope

Every webhook delivery uses the same top-level structure:

{
  "delivery_id": "d4f5a6b7-c8d9-4e0f-a1b2-c3d4e5f6a7b8",
  "event": "job.completed",
  "event_version": 1,
  "timestamp": "2026-03-20T12:00:00+00:00",
  "data": {
    "...": "event-specific fields"
  }
}
Field Type Description
delivery_id string Unique ID for this delivery attempt (UUIDv4). Use for idempotency.
event string Event type (see below).
event_version integer Schema version for the event payload (currently 1).
timestamp string ISO 8601 timestamp when the event was created.
data object Event-specific payload.

Events

job.completed

Fired when a translation job finishes successfully (status completed or completed_with_blocks).

{
  "delivery_id": "d4f5a6b7-c8d9-4e0f-a1b2-c3d4e5f6a7b8",
  "event": "job.completed",
  "event_version": 1,
  "timestamp": "2026-03-20T12:00: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://app.falara.io/v1/jobs/550e8400-.../result",
    "download_url": "https://app.falara.io/v1/jobs/550e8400-.../download"
  }
}

job.failed

Fired when a job fails unrecoverably (status failed or dead).

{
  "delivery_id": "...",
  "event": "job.failed",
  "event_version": 1,
  "timestamp": "2026-03-20T12: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": true,
    "result_url": "https://app.falara.io/v1/jobs/550e8400-.../result",
    "download_url": "https://app.falara.io/v1/jobs/550e8400-.../download"
  }
}

job.needs_review

Fired when QA score is below threshold after correction loops. The translation is available but may need manual review.

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

batch.completed

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

{
  "delivery_id": "...",
  "event": "batch.completed",
  "event_version": 1,
  "timestamp": "2026-03-20T12:15:00+00:00",
  "data": {
    "batch_id": "batch_abc123",
    "total_jobs": 5,
    "completed": 4,
    "failed": 0,
    "needs_review": 1,
    "download_url": "https://app.falara.io/v1/jobs/batch/batch_abc123/download"
  }
}

Status to 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 (no event)
processing (no event)

Delivery

Retry Policy

Failed deliveries are retried with exponential backoff:

Attempt Delay
1st retry 30 seconds
2nd retry 2 minutes
3rd retry 10 minutes
4th retry 30 minutes
5th retry 2 hours

After 5 retries, the delivery is permanently dropped and logged.

Non-Retryable Status Codes

The following HTTP responses from your endpoint are treated as permanent failures (no retry):

Status Code Meaning
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
410 Gone
422 Unprocessable Entity

All other non-2xx responses (including 429 and 5xx) trigger a retry.

Duplicate-Event Protection

Each (job_id, event) pair is delivered at most once. Duplicate enqueue calls within 24 hours are silently ignored. Use the delivery_id field for additional idempotency on your end.


Your Endpoint Requirements

Your webhook endpoint must:

  1. Accept POST requests with Content-Type: application/json.
  2. Return 2xx within 3 seconds. If your processing takes longer, accept the request immediately and process asynchronously.
  3. Use HTTPS. HTTP endpoints are rejected during configuration.
  4. Be publicly reachable. URLs resolving to private/internal networks are blocked (SSRF protection).
  5. Verify the signature using the X-Falara-Signature and X-Falara-Timestamp headers.

Idempotent processing

Although Falara delivers each event at most once, network issues could theoretically cause a duplicate. Use delivery_id to deduplicate on your side.

Timeout

If your endpoint does not respond within 3 seconds, the delivery is treated as a failure and will be retried. Keep processing fast or use a job queue.