llms.txtllms-full.txt
DashboardStatusGet API Key
IntroductionQuickstartModelsPricingArchitecture & SecurityLimits & Quotas
Execution Modes & HTTP QueueWebhooksWebSocketsMCP Servern8n Integrationn8n dryAPI node
API OverviewErrorsText-to-ImagePOSTText-to-Image Price CalculationPOSTText-to-VideoPOSTText-to-Video Price CalculationPOSTImage-to-VideoPOSTImage-to-Video Price CalculationPOSTAudio-to-VideoPOSTAudio-to-Video Price CalculationPOSTText-to-Speech (TTS)POSTText-to-Speech Price CalculationPOSTText-to-MusicPOSTText-to-Music Price CalculationPOSTText-to-EmbeddingPOSTText-to-Embedding Price CalculationPOSTImage-to-ImagePOSTImage-to-Image Price CalculationPOSTImage Background RemovalPOSTImage Background Removal Price CalculationPOSTImage UpscalePOSTImage Upscale Price CalculationPOST
OpenAPI
SDKs & IntegrationsPayment MethodsFAQ — Frequently Asked QuestionsSupport & Contact
dAdryAPI
DashboardStatusGet API Key
Execution Modes & Integrations
Technical Reference

Webhooks

Technical documentation for dryAPI APIs, integration guides, and operational references.

Receive real-time notifications about job status changes

Overview

Webhooks allow you to receive real-time HTTP notifications when your inference jobs change status, eliminating the need to poll the /request-status endpoint.

INFO

Webhooks are sent as POST requests to your specified URL with a JSON payload and security headers for verification.

Configuration

Global Webhook URL

Configure a global webhook URL in your account settings. All jobs will send notifications to this URL unless overridden.

Per-Request Override

You can override the global webhook URL on any job request by including the webhook_url parameter:

{
  "prompt": "a beautiful sunset over mountains",
  "model": "flux-2-klein-4b-bf16",
  "webhook_url": "https://your-server.com/webhooks/dryapi"
}

WARNING

Only HTTPS URLs are accepted. HTTP URLs will be rejected for security.

Events

Webhooks are sent on the following status transitions:

EventTriggerDescription
job.processingpending → processingJob assigned to a worker, processing started
job.completedprocessing → doneJob completed successfully, results available
job.failedprocessing → errorJob failed during processing

Request Format

Headers

All webhook requests include these headers:

HeaderDescriptionExample
X-DryAPI-SignatureHMAC-SHA256 signature for verificationsha256=5d41402abc4b2a76b9719d911017c592...
X-DryAPI-TimestampUnix timestamp when webhook was sent1705315800
X-DryAPI-EventEvent type that triggered the webhookjob.completed
X-DryAPI-Delivery-IdUnique identifier for this delivery550e8400-e29b-41d4-a716-446655440001
Content-TypeAlways application/jsonapplication/json
User-AgentIdentifies the senderDryAPI-Webhook/1.0

Payload Structure

Sent when a worker starts processing your job.

{
  "event": "job.processing",
  "delivery_id": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "job_request_id": "123e4567-e89b-12d3-a456-426614174000",
    "status": "processing",
    "previous_status": "pending",
    "job_type": "txt2img",
    "started_at": "2024-01-15T10:30:00.000Z"
  }
}
FieldTypeDescription
data.job_request_idstring (UUID)Your job's unique identifier
data.statusstringCurrent status: processing
data.previous_statusstringPrevious status: pending
data.job_typestringType of job (e.g., txt2img, vid2txt)
data.started_atstring (ISO 8601)When processing began

Sent when your job completes successfully.

{
  "event": "job.completed",
  "delivery_id": "550e8400-e29b-41d4-a716-446655440001",
  "timestamp": "2024-01-15T10:30:45.000Z",
  "data": {
    "job_request_id": "123e4567-e89b-12d3-a456-426614174000",
    "status": "done",
    "previous_status": "processing",
    "job_type": "txt2img",
    "completed_at": "2024-01-15T10:30:45.000Z",
    "result_url": "https://storage.dryapi.dev/results/.../output.png",
    "processing_time_ms": 45000
  }
}
FieldTypeDescription
data.job_request_idstring (UUID)Your job's unique identifier
data.statusstringCurrent status: done
data.previous_statusstringPrevious status: processing
data.job_typestringType of job
data.completed_atstring (ISO 8601)When job completed
data.result_urlstring (URL)URL to download the result
data.processing_time_msintegerProcessing time in milliseconds

Sent when your job fails during processing.

{
  "event": "job.failed",
  "delivery_id": "550e8400-e29b-41d4-a716-446655440002",
  "timestamp": "2024-01-15T10:30:45.000Z",
  "data": {
    "job_request_id": "123e4567-e89b-12d3-a456-426614174000",
    "status": "error",
    "previous_status": "processing",
    "job_type": "txt2img",
    "failed_at": "2024-01-15T10:30:45.000Z",
    "error_code": "WORKER_TIMEOUT",
    "error_message": "Worker failed to respond within the allowed time"
  }
}
FieldTypeDescription
data.job_request_idstring (UUID)Your job's unique identifier
data.statusstringCurrent status: error
data.previous_statusstringPrevious status: processing
data.job_typestringType of job
data.failed_atstring (ISO 8601)When the job failed
data.error_codestringMachine-readable error code
data.error_messagestringHuman-readable error description

Error Codes:

CodeDescription
WORKER_TIMEOUTWorker didn't respond in time
PROCESSING_ERRORError during inference
AGE_RESTRICTEDContent flagged as age-restricted
CONTEXT_LENGTH_EXCEEDEDInput exceeded model's context limit
INVALID_INPUTInvalid input data
UNKNOWN_ERRORUnexpected error

Security

Signature Verification

Every webhook includes an HMAC-SHA256 signature in the X-DryAPI-Signature header. Always verify this signature to ensure the request came from dryAPI.

Signature format: sha256=<hex-encoded-hmac>

How to verify:

  1. Get the timestamp from X-DryAPI-Timestamp header
  2. Get the raw JSON body (don't parse it first)
  3. Concatenate: timestamp + "." + raw_body
  4. Calculate HMAC-SHA256 using your webhook secret
  5. Compare with the signature (use timing-safe comparison)
const crypto = require('crypto');

function verifyWebhook(req, secret) {
  const signature = req.headers['x-dryapi-signature'];
  const timestamp = req.headers['x-dryapi-timestamp'];
  const body = req.body.toString(); // Raw request body as string

  // Check timestamp is within 5 minutes
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return false; // Reject old requests (replay attack prevention)
  }

  // Calculate expected signature
  const message = `${timestamp}.${body}`;
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(message)
    .digest('hex');

  // Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}
import hmac
import hashlib
import time

def verify_webhook(headers, body, secret):
    signature = headers.get('X-DryAPI-Signature', '')
    timestamp = headers.get('X-DryAPI-Timestamp', '')

    # Check timestamp is within 5 minutes
    now = int(time.time())
    if abs(now - int(timestamp)) > 300:
        return False  # Reject old requests

    # Calculate expected signature
    message = f"{timestamp}.{body}"
    expected = 'sha256=' + hmac.new(
        secret.encode(),
        message.encode(),
        hashlib.sha256
    ).hexdigest()

    # Timing-safe comparison
    return hmac.compare_digest(signature, expected)
function verifyWebhook(array $headers, string $body, string $secret): bool
{
    $signature = $headers['X-DryAPI-Signature'] ?? '';
    $timestamp = $headers['X-DryAPI-Timestamp'] ?? '';

    // Check timestamp is within 5 minutes
    if (abs(time() - (int) $timestamp) > 300) {
        return false; // Reject old requests
    }

    // Calculate expected signature
    $message = $timestamp . '.' . $body;
    $expected = 'sha256=' . hash_hmac('sha256', $message, $secret);

    // Timing-safe comparison
    return hash_equals($expected, $signature);
}
package main

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

func verifyWebhook(signature, timestamp, body, secret string) bool {
    // Check timestamp is within 5 minutes
    ts, _ := strconv.ParseInt(timestamp, 10, 64)
    now := time.Now().Unix()
    if math.Abs(float64(now-ts)) > 300 {
        return false
    }

    // Calculate expected signature
    message := fmt.Sprintf("%s.%s", timestamp, body)
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(message))
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))

    // Timing-safe comparison
    return hmac.Equal([]byte(signature), []byte(expected))
}

Best Practices

Never process webhooks without verifying the HMAC signature first.

Reject webhooks with timestamps older than 5 minutes to prevent replay attacks.

Only configure HTTPS endpoints. HTTP is rejected automatically.

Use delivery_id to detect and handle duplicate deliveries.

Retry Policy

If your endpoint fails to respond with a 2xx status code, we'll retry delivery with exponential backoff:

AttemptDelayCumulative Time
1Immediate0
21 minute1 min
32 minutes3 min
45 minutes8 min
510 minutes18 min
630 minutes48 min
71 hour1h 48min
83 hours4h 48min
96 hours10h 48min
1012 hours22h 48min

After 10 failed attempts (~24 hours), the webhook is marked as failed.

WARNING

Circuit Breaker: After 10 consecutive failed deliveries across any webhooks for your account, webhooks are automatically disabled. Re-enable them in your account settings after fixing your endpoint.

Response Requirements

Your endpoint should:

  • Return a 2xx status code (200-299) within 10 seconds
  • Not follow redirects (3xx responses are treated as failures)
  • Process the webhook asynchronously if needed (respond quickly, process later)
// Example: Express.js endpoint
app.post('/webhooks/dryapi', express.raw({ type: 'application/json' }), (req, res) => {
  // Verify signature first
  if (!verifyWebhook(req, process.env.DRYAPI_WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);

  // Respond immediately
  res.status(200).send('OK');

  // Process asynchronously
  processWebhookAsync(event);
});

Testing

Use webhook.site to test webhooks during development:

  1. Get a unique URL from webhook.site
  2. Configure it as your webhook URL (per-request or global)
  3. Submit a job and watch the webhooks arrive
  4. Verify headers and payloads match the expected format

TIP

You can also use tools like ngrok to expose your local development server to receive webhooks.

Last updated on 21 March 2026

Execution Modes & HTTP Queue

Previous Page

WebSockets

Next Page

On this page

OverviewConfigurationGlobal Webhook URLPer-Request OverrideEventsRequest FormatHeadersPayload StructureSecuritySignature VerificationBest PracticesRetry PolicyResponse RequirementsTesting