REST API
Webhooks

Webhooks

HeyDonna sends webhook notifications when project status changes. Configure a webhook URL in your API key settings to receive events.

Payload Format

All events share the same envelope:

{
  "event": "project.ready",
  "projectId": "j57abc123def456",
  "externalRef": "ORG-JOB-98765",
  "status": "ready",
  "timestamp": "2024-06-15T14:32:10.000Z"
}
FieldTypeDescription
eventstringEvent type (see table below).
projectIdstringThe project ID (from POST /api/projects).
externalRefstring | nullYour externalRef from project creation.
statusstringCurrent project status at time of event.
timestampstring (ISO 8601)When the event was generated.

Events

EventWhen
project.readyAI processing complete; transcript ready for editing.
project.failedProcessing failed (transcription or AI pipeline error).

Webhook Delivery

HeyDonna uses the Convex scheduler for webhook delivery. Failed deliveries (non-2xx responses or timeouts) are automatically retried with exponential backoff.

Return 200 OK within 5 seconds to acknowledge receipt. Enqueue any slow processing asynchronously — the retry window is limited.

Webhook Header

Each request includes an HMAC-SHA256 signature:

X-HeyDonna-Signature: sha256=<hex>

Verifying Signatures

Compute HMAC-SHA256 of the raw request body using your webhook secret, then compare with the X-HeyDonna-Signature header value.

⚠️

Always verify the signature before processing the payload. Requests without a valid signature should be rejected with 403.

import hmac
import hashlib
 
def verify_webhook(payload_bytes: bytes, secret: str, signature_header: str) -> bool:
    """
    Verify a HeyDonna webhook signature.
 
    Args:
        payload_bytes: Raw request body (bytes, not decoded).
        secret: Webhook secret configured for this API key.
        signature_header: Value of the X-HeyDonna-Signature header.
 
    Returns:
        True if the signature is valid.
    """
    expected = hmac.new(
        secret.encode("utf-8"),
        payload_bytes,
        hashlib.sha256,
    ).hexdigest()
 
    return hmac.compare_digest(f"sha256={expected}", signature_header)
 
 
# Flask example
from flask import Flask, request, abort
import json
 
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["HEYDONNA_WEBHOOK_SECRET"]
 
@app.route("/webhooks/heydonna", methods=["POST"])
def handle_webhook():
    payload_bytes = request.get_data()
    signature = request.headers.get("X-HeyDonna-Signature", "")
 
    if not verify_webhook(payload_bytes, WEBHOOK_SECRET, signature):
        abort(403)
 
    event = request.get_json()
    if event["event"] == "project.ready":
        project_id = event["projectId"]
        print(f"Project {project_id} is ready for editing")
    elif event["event"] == "project.failed":
        print(f"Project {event['projectId']} failed processing")
 
    return "", 200  # Always return 200 quickly

Reliability

Always return 200 OK within 5 seconds. HeyDonna retries failed deliveries (non-2xx responses) automatically via the Convex scheduler with exponential backoff. Make your handler idempotent — the same event may be delivered more than once. Use an async queue (Celery, Sidekiq, BullMQ) to process the event and return 200 immediately.

Recommended Pattern

@app.route("/webhooks/heydonna", methods=["POST"])
def handle_webhook():
    payload_bytes = request.get_data()
    signature = request.headers.get("X-HeyDonna-Signature", "")
 
    # 1. Verify signature synchronously (fast)
    if not verify_webhook(payload_bytes, WEBHOOK_SECRET, signature):
        abort(403)
 
    # 2. Enqueue for async processing (fast)
    payload = request.get_json()
    queue.enqueue(process_webhook_event, payload)
 
    # 3. Return 200 immediately (before processing)
    return "", 200

Alternative: Polling

If webhooks are not feasible, you can poll GET /api/projects/:id/status until status reaches a terminal state (ready, transcribed, finalized, or failed). The status endpoint is not rate-limited.