Async Translation with Webhooks

Translation is asynchronous. Instead of polling for job status, you can register a webhook URL and receive an HTTP POST when a job finishes. DocsRTL sends a signed payload to your endpoint as soon as the translation completes or fails.

The async pattern

Three steps: submit the job, let it run, receive the result.

# 1. Submit the document
POST /v1/translate
X-API-Key: drtl_your_key
Content-Type: multipart/form-data

→ { "job_id": "abc123", "status": "pending" }

# 2. Job runs asynchronously (no polling needed if you use webhooks)

# 3. DocsRTL POSTs to your registered URL when done
← POST https://your-server.com/webhook
   Content-Type: application/json
   X-Webhook-Event: job.completed
   X-Webhook-Signature: sha256=abc...

   { "id": "abc123", "event": "job.completed", ... }

Register a webhook endpoint

Register your URL via the API or from your account settings.

POST /api/webhooks
X-API-Key: drtl_your_key
Content-Type: application/json

{
  "url": "https://your-server.com/webhook",
  "events": ["job.completed", "job.failed"]
}

→ {
  "id": "wh_abc123",
  "url": "https://your-server.com/webhook",
  "events": ["job.completed", "job.failed"],
  "secret": "64_char_hex_secret_shown_only_once",
  "is_active": true,
  "created_at": "2026-04-19T10:00:00Z"
}

Save the secret - it is shown only once and is used to verify webhook signatures.

Event types

EventWhen fired
job.completedTranslation finished successfully and file is ready to download
job.failedTranslation failed (quota exceeded, corrupt file, or internal error)

Payload format

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "event": "job.completed",
  "timestamp": "2026-04-19T12:34:56.789Z",
  "data": {
    "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "user_id": "user_uuid",
    "status": "completed",
    "original_name": "contract.docx",
    "docx_url": "https://docsrtl.com/v1/jobs/3fa85f64.../download",
    "pdf_url": null,
    "total_words": 4200,
    "target_language": "Arabic",
    "error": null
  }
}

For job.failed events, data.error contains a human-readable error message and data.docx_url is null.

Verifying signatures

Every webhook request includes an X-Webhook-Signature header containing an HMAC-SHA256 signature of the raw request body. Always verify it before processing the payload.

Python

import hashlib
import hmac
from fastapi import Request, HTTPException

WEBHOOK_SECRET = "your_64_char_secret"

async def verify_webhook(request: Request):
    body = await request.body()
    sig_header = request.headers.get("X-Webhook-Signature", "")

    if not sig_header.startswith("sha256="):
        raise HTTPException(status_code=401, detail="Missing signature")

    expected = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode(),
        body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, sig_header):
        raise HTTPException(status_code=401, detail="Invalid signature")

Node.js

const crypto = require('crypto');

function verifyWebhook(rawBody, signatureHeader, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  const provided = Buffer.from(signatureHeader);
  const computed = Buffer.from(expected);

  if (provided.length !== computed.length) return false;
  return crypto.timingSafeEqual(provided, computed);
}

// Express handler
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyWebhook(req.body, req.headers['x-webhook-signature'], WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  const payload = JSON.parse(req.body);
  // handle payload...
  res.sendStatus(200);
});

Full Python handler example

import hashlib
import hmac
import httpx
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks

app = FastAPI()
WEBHOOK_SECRET = "your_64_char_secret"
DOCSRTL_API_KEY = "drtl_your_key"

async def download_translated_file(download_url: str) -> bytes:
    async with httpx.AsyncClient() as client:
        r = await client.get(download_url, headers={"X-API-Key": DOCSRTL_API_KEY})
        r.raise_for_status()
        return r.content

async def process_completed_job(data: dict):
    download_url = data["docx_url"]
    filename = data["original_name"]
    file_bytes = await download_translated_file(download_url)
    # Save to storage, notify user, etc.
    print(f"Downloaded {filename}: {len(file_bytes)} bytes")

@app.post("/webhook")
async def handle_webhook(request: Request, background_tasks: BackgroundTasks):
    body = await request.body()
    sig = request.headers.get("X-Webhook-Signature", "")

    expected = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode(), body, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(expected, sig):
        raise HTTPException(status_code=401, detail="Bad signature")

    payload = request.json()
    event = payload.get("event")
    data = payload.get("data", {})

    if event == "job.completed":
        background_tasks.add_task(process_completed_job, data)
    elif event == "job.failed":
        print(f"Job failed: {data.get('error')}")

    return {"ok": True}

LangChain async tool example

Use DocsRTL as an async LangChain tool with webhook-driven completion:

import asyncio
import httpx
from langchain.tools import BaseTool
from typing import Optional

DOCSRTL_BASE = "https://docsrtl.com/v1"
API_KEY = "drtl_your_key"

class DocsRTLTranslateTool(BaseTool):
    name = "translate_document"
    description = "Translates a DOCX file to Hebrew, Arabic, Persian, or Urdu."

    def _run(self, file_path: str, target_language: str) -> str:
        return asyncio.run(self._arun(file_path, target_language))

    async def _arun(self, file_path: str, target_language: str) -> str:
        headers = {"X-API-Key": API_KEY}

        async with httpx.AsyncClient(timeout=30) as client:
            # Submit translation job
            with open(file_path, "rb") as f:
                r = await client.post(
                    f"{DOCSRTL_BASE}/translate",
                    headers=headers,
                    files={"file": (file_path, f)},
                    data={"target_lang": target_language},
                )
            r.raise_for_status()
            job_id = r.json()["job_id"]

            # Poll until done (or use webhooks for production)
            for _ in range(60):
                await asyncio.sleep(5)
                status_r = await client.get(
                    f"{DOCSRTL_BASE}/jobs/{job_id}", headers=headers
                )
                job = status_r.json()
                if job["status"] == "completed":
                    return f"Translation complete. Download: {job['download_url']}"
                if job["status"] == "failed":
                    return f"Translation failed: {job.get('error', 'unknown error')}"

            return f"Job {job_id} still running after 5 minutes"

Retry behavior

If your endpoint returns a non-2xx response, DocsRTL retries with exponential backoff:

AttemptDelay
1st (initial)Immediate
2nd2 seconds
3rd4 seconds

After 3 failed attempts, the delivery is abandoned. Each attempt is logged in your delivery history. Request timeout per attempt: 15 seconds.

Manage your webhook endpoints - add URLs, select events, send test pings - from account settings.

Manage webhooks