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"
}| Field | Type | Description |
|---|---|---|
event | string | Event type (see table below). |
projectId | string | The project ID (from POST /api/projects). |
externalRef | string | null | Your externalRef from project creation. |
status | string | Current project status at time of event. |
timestamp | string (ISO 8601) | When the event was generated. |
Events
| Event | When |
|---|---|
project.ready | AI processing complete; transcript ready for editing. |
project.failed | Processing 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 quicklyReliability
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 "", 200Alternative: 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.