Webhooks
Webhooks let you receive a notification when a job reaches a terminal state (completed, failed, rejected, or cancelled). Instead of polling GET /api/v1/jobs/:id, Xora sends a POST to your URL with the job result.
Setting a webhook
Section titled “Setting a webhook”Per-job webhook
Section titled “Per-job webhook”Add webhookUrl to your job creation request:
curl -X POST https://api.xora.sh/v1/jobs \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "mode": "recipe", "input": { "url": "https://example.com/video.mp4" }, "output": { "format": "mp4" }, "recipe": { "name": "compress", "crf": 28 }, "webhookUrl": "https://your-server.com/webhooks/xora" }'const response = await fetch('https://api.xora.sh/v1/jobs', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ mode: 'recipe', input: { url: 'https://example.com/video.mp4' }, output: { format: 'mp4' }, recipe: { name: 'compress', crf: 28 }, webhookUrl: 'https://your-server.com/webhooks/xora' })});const data = await response.json();console.log(data);import requests
response = requests.post( "https://api.xora.sh/v1/jobs", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "mode": "recipe", "input": { "url": "https://example.com/video.mp4" }, "output": { "format": "mp4" }, "recipe": { "name": "compress", "crf": 28 }, "webhookUrl": "https://your-server.com/webhooks/xora" })data = response.json()print(data)Default webhook
Section titled “Default webhook”You can set a default webhook URL that’s used for all jobs that don’t specify their own webhookUrl:
curl -X POST https://api.xora.sh/v1/settings \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "defaultWebhookUrl": "https://your-server.com/webhooks/xora" }'const response = await fetch('https://api.xora.sh/v1/settings', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ defaultWebhookUrl: 'https://your-server.com/webhooks/xora' })});const data = await response.json();console.log(data);import requests
response = requests.post( "https://api.xora.sh/v1/settings", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "defaultWebhookUrl": "https://your-server.com/webhooks/xora" })data = response.json()print(data)When a job includes a webhookUrl, it overrides the default. When it doesn’t, the default is used.
Webhook payload
Section titled “Webhook payload”When a job reaches a terminal state, Xora sends a POST request to your URL with this JSON body:
{ "data": { "jobId": "01JXYZ1234ABCDEF56789000", "state": "completed", "progress": 100, "output": { "signedUrl": "https://cdn.xora.sh/...signed-url..." }, "output_files": { "output": { "file_id": "01JXYZ...-output", "filename": "output.mp4", "size_mbytes": 12.5, "duration": 120.5, "file_type": "video", "file_format": "mp4", "mime_type": "video/mp4", "codec": "h264", "width": 1920, "height": 1080, "storage_url": "https://cdn.xora.sh/...signed-url..." } } }, "timestamp": 1717500000000}Failed job payload
Section titled “Failed job payload”{ "data": { "jobId": "01JXYZ1234ABCDEF56789000", "state": "failed", "progress": 35, "error": { "code": "TRANSCODE_ERROR", "message": "FFmpeg exited with code 1", "stage": "transcoding", "retryable": true } }, "timestamp": 1717500000000}Payload fields
Section titled “Payload fields”| Field | Type | Description |
|---|---|---|
data.jobId | string | The job ID |
data.state | string | Terminal state: completed, failed, rejected, cancelled |
data.progress | number | Progress at time of completion (100 for completed) |
data.output | object | Signed URL for single-output jobs |
data.outputs | object | Signed URLs for multi-output jobs |
data.output_files | object | Detailed file metadata per output |
data.error | object | Error details (only present on failure) |
timestamp | number | Unix timestamp in milliseconds |
Requirements
Section titled “Requirements”Retry behavior
Section titled “Retry behavior”If your endpoint returns a non-2xx response or is unreachable, Xora retries the webhook with exponential backoff:
| Attempt | Delay after failure |
|---|---|
| 1st | Immediate |
| 2nd | 500ms |
| 3rd | 1 second |
After 3 failed attempts, the webhook is abandoned. The job result is still available via GET /api/v1/jobs/:id.
Testing your webhook
Section titled “Testing your webhook”Use the webhook test endpoint to send a sample payload to your configured default URL:
curl -X POST https://api.xora.sh/v1/settings/webhook/test \ -H "Authorization: Bearer YOUR_API_KEY"const response = await fetch('https://api.xora.sh/v1/settings/webhook/test', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY' }});const data = await response.json();console.log(data);import requests
response = requests.post( "https://api.xora.sh/v1/settings/webhook/test", headers={ "Authorization": "Bearer YOUR_API_KEY" })data = response.json()print(data)Success response
Section titled “Success response”{ "ok": true, "status": 200}Failure response
Section titled “Failure response”{ "ok": false, "status": 500}Checking your settings
Section titled “Checking your settings”Read your current webhook configuration:
curl https://api.xora.sh/v1/settings \ -H "Authorization: Bearer YOUR_API_KEY"const response = await fetch('https://api.xora.sh/v1/settings', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' }});const data = await response.json();console.log(data);import requests
response = requests.get( "https://api.xora.sh/v1/settings", headers={ "Authorization": "Bearer YOUR_API_KEY" })data = response.json()print(data){ "defaultWebhookUrl": "https://your-server.com/webhooks/xora", "defaultStorageProviderId": null, "updatedAt": "2026-06-04T10:00:00.000Z"}Clearing the default webhook
Section titled “Clearing the default webhook”Set defaultWebhookUrl to null:
curl -X PUT https://api.xora.sh/v1/settings \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "defaultWebhookUrl": null }'const response = await fetch('https://api.xora.sh/v1/settings', { method: 'PUT', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ defaultWebhookUrl: null })});const data = await response.json();console.log(data);import requests
response = requests.put( "https://api.xora.sh/v1/settings", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "defaultWebhookUrl": None })data = response.json()print(data)