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
| Event | When fired |
|---|---|
| job.completed | Translation finished successfully and file is ready to download |
| job.failed | Translation 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:
| Attempt | Delay |
|---|---|
| 1st (initial) | Immediate |
| 2nd | 2 seconds |
| 3rd | 4 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