This feature is currently in beta and requires approval.

Overview

Webhooks allow you to receive real-time notifications when your Parallel Task Runs complete, eliminating the need for constant polling—especially for long-running or research-intensive tasks. Our webhooks follow standard webhook conventions to ensure security and interoperability. Once webhooks are enabled for your account, you can securely send notifications to any URL of your choice.

Setup

1. Contact Support

Email support@parallel.ai to enable webhooks for your account.

2. Receive Account Secret

Upon approval, you will receive a unique account secret via email. This secret is used to sign webhook requests for security purposes.

3. Configure Webhook in Request

Webhooks must be configured on a per-run basis. To register a callback for a task run, include a webhook object in your task-run creation request:
curl --request POST \
  --url https://api.parallel.ai/v1beta/tasks/runs \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: YOUR_API_KEY' \
  --data '{
    "task_spec": {
      "output_schema": "Find the GDP of the specified country and year"
    },
    "input": "France (2023)",
    "processor": "core",
    "metadata": {
      "key": "value"
    },
    "webhook": {
      "url": "https://your-domain.com/webhooks/parallel",
      "event_types": ["task_run.status"],
      "secret": "your-optional-custom-secret"
    }
  }'

Webhook Parameters

ParameterTypeRequiredDescription
urlstringYesYour webhook endpoint URL. Can be any domain.
event_typesarray[string]YesCurrently only ["task_run.status"] is supported.
secretstringNoOptional custom secret. If not provided, your account default secret will be used.

Webhook Events

Task Run Status

We currently only support task_run.status event on task run completions. When a task run finishes, either with a success or a failure, we will send a POST request to your configured webhook endpoint.

Response Format

Request Headers

Your webhook endpoint will receive requests with these headers:
  • webhook-id: Unique identifier for each webhook event
  • webhook-timestamp: Unix timestamp in seconds
  • webhook-signature: Versioned signature, e.g. v1,<base64 signature>
{
  "Content-Type": "application/json",
  "webhook-id": "whevent_abc123def456",
  "webhook-timestamp": "1751498975",
  "webhook-signature": "v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4="
}

Webhook Payload Structure

Each webhook payload contains the following fields:
  • timestamp: ISO 8601 timestamp of when the event occurred
  • type: Event type (currently only task_run.status is supported)
  • data: Event-specific payload. For the ‘task_run.status’ event, it is the complete Task Run object

Example Payloads

The following examples demonstrate the payload structure for completed and failed task runs:
Success
{
  "timestamp": "2025-04-23T20:21:48.037943Z",
  "type": "task_run.status",
  "data": {
    "run_id": "trun_9907962f83aa4d9d98fd7f4bf745d654",
    "status": "completed",
    "is_active": false,
    "warnings": null,
    "error": null,
    "processor": "core",
    "metadata": {
      "key": "value"
    },
    "created_at": "2025-04-23T20:21:48.037943Z",
    "modified_at": "2025-04-23T20:21:48.037943Z"
  }
}
Failure
{
  "timestamp": "2025-04-23T20:21:48.037943Z",
  "type": "task_run.status",
  "data": {
    "run_id": "trun_9907962f83aa4d9d98fd7f4bf745d654",
    "status": "failed",
    "is_active": false,
    "warnings": null,
    "error": {
      "message": "Task execution failed",
      "details": "Additional error details"
    },
    "processor": "core",
    "metadata": {
      "key": "value"
    },
    "created_at": "2025-04-23T20:21:48.037943Z",
    "modified_at": "2025-04-23T20:21:48.037943Z"
  }
}

Security & Reliability

HMAC Signature Verification

Webhook requests are signed using HMAC-SHA256. The signature header is formatted as v1,<base64 signature> where <base64 signature> is signed with the following payload
<webhook-id>.<webhook-timestamp>.<payload>
Where:
  • <webhook-id>: The value of the webhook-id header
  • <webhook-timestamp>: The value of the webhook-timestamp header
  • <payload>: The exact JSON body of the webhook request
You must parse the version and the signature before verifying. Here’s how to verify the signature:
#!/bin/bash

# Get the signature and metadata from the webhook headers
HEADER_SIGNATURE="$WEBHOOK_SIGNATURE_HEADER"  # e.g. v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
WEBHOOK_ID="$WEBHOOK_ID_HEADER"               # e.g. whevent_2KWPBgLlAfxdpx2AI54pPJ85f4W
WEBHOOK_TIMESTAMP="$WEBHOOK_TIMESTAMP_HEADER" # e.g. 1674087231

# Parse version and signature
VERSION=$(echo "$HEADER_SIGNATURE" | cut -d',' -f1)
RECEIVED_SIGNATURE=$(echo "$HEADER_SIGNATURE" | cut -d',' -f2)

# Read the minified JSON payload from stdin
PAYLOAD=$(cat /dev/stdin)

# Concatenate for signing: <id>.<timestamp>.<payload>
TO_SIGN="$WEBHOOK_ID.$WEBHOOK_TIMESTAMP.$PAYLOAD"

SECRET="your-secret-key"
GENERATED_SIGNATURE=$(echo -n "$TO_SIGN" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)

# Compare signatures
if [ "$GENERATED_SIGNATURE" = "$RECEIVED_SIGNATURE" ]; then
    echo "✅ Signature verification successful (version: $VERSION)"
else
    echo "❌ Signature verification failed"
    echo "Generated: $GENERATED_SIGNATURE"
    echo "Received:  $RECEIVED_SIGNATURE"
    exit 1
fi

Retry Policy

Webhook delivery uses the following retry configuration:
  • Initial delay: 5 seconds
  • Backoff strategy: Exponential backoff (doubles per failed request)
  • Maximum retries: Multiple attempts over 48 hours
After exhausting all retry attempts, webhook delivery for that event is terminated.

Best Practices

1. Always Return 2xx Status

Your webhook endpoint should return a 2xx HTTP status code to acknowledge receipt. Any other status code will trigger retries.

2. Verify Signatures

Always verify HMAC signatures using the correct secret to ensure webhook authenticity. Ensure that you are calculating signatures using the proper process as shown above. Remember that if you don’t provide a secret in your request, the default secret is always used.

3. Handle Duplicates

Although not common, duplicate events may be sent to the configured webhook URL. Ensure your webhook handler can detect and safely ignore duplicate events.

4. Process Asynchronously

Process webhook events asynchronously to avoid timeouts and ensure quick response times.

Example Webhook Handler

Here’s a simple example of a webhook handler in Python:
from flask import Flask, request, jsonify

app = Flask(__name__)

WEBHOOK_SECRET = "your-secret-key"

def verify_webhook_signature(webhook_id, webhook_timestamp, payload, secret, header_signature):
    try:
        version, received_signature = header_signature.split(',', 1)
    except ValueError:
        return False
    to_sign = f"{webhook_id}.{webhook_timestamp}.{payload}"
    import hmac, hashlib, base64
    expected_signature = base64.b64encode(
        hmac.new(
            secret.encode('utf-8'),
            to_sign.encode('utf-8'),
            hashlib.sha256
        ).digest()
    ).decode('utf-8')
    return hmac.compare_digest(expected_signature, received_signature)

@app.route('/webhooks/parallel', methods=['POST'])
def webhook_handler():
    # Extract headers
    webhook_id = request.headers.get('webhook-id')
    webhook_timestamp = request.headers.get('webhook-timestamp')
    header_signature = request.headers.get('webhook-signature')
    # Get the minified JSON body as string
    payload = request.get_data(as_text=True)
    # Verify signature
    if header_signature and not verify_webhook_signature(webhook_id, webhook_timestamp, payload, WEBHOOK_SECRET, header_signature):
        return jsonify({"error": "Invalid signature"}), 401
    # Parse the webhook data
    data = request.json
    # Process the webhook event
    if data['type'] == 'task_run.status':
        run_id = data['data']['run_id']
        status = data['data']['status']
        if status == 'completed':
            print(f"✅ Task {run_id} completed successfully")
            # Handle successful completion
        elif status == 'failed':
            print(f"❌ Task {run_id} failed")
            # Handle failure
    # Always return 200 to acknowledge receipt
    return jsonify({"status": "received"}), 200

if __name__ == '__main__':
    app.run(port=5000)