Durable Functions wait for external event: AI video webhook
A Durable Functions orchestrator waits for an external event that your webhook function raises, racing a durable timer for runs that never POST.

In Azure Durable Functions, an orchestrator waits for an external event with wait_for_external_event (waitForExternalEvent in JavaScript), and a client function raises that event on the orchestration's instance id with raise_event. The wait has no deadline by default, so race it against a durable timer. To wait for an AI video job, an activity starts the job and an HTTP-triggered function raises the event when the job's webhook arrives.
Azure facts come from Microsoft Learn's external events, durable timers, code constraints, bindings, Durable Functions HTTP API, and HTTP trigger pages and the Python orchestration context and client references; Sume facts from Create a run, Runs and results, and Run webhooks. All were read on 2026-09-27. Sume has no Azure connector: your functions call its HTTPS API.
How do the pieces fit a Sume video run?
An orchestrator, two activities, and an HTTP-triggered function, each with one job. Orchestrator code must be deterministic, so the network calls live in activities.
| Piece | Durable Functions rule | Sume side |
|---|---|---|
start_sume_run activity | Outbound network calls belong in activities, not orchestrators. | Same Idempotency-Key, same body: the original run comes back, with no second charge. |
wait_for_external_event("SumeRunEnded") | Waits indefinitely. An event raised before the orchestrator listens is queued until it does. | Sume POSTs once, when the run completes or fails. |
create_timer raced with task_any | Cancel the timer if the event wins, or the instance stays alive until it fires. | A run is force-finalized as failed 90 minutes after created_at. |
HTTP function calling raise_event | Events are at-least-once; raise_event raises on a 404 or 400. | Answer 2xx within 10 seconds; Sume retries up to 10 attempts. |
What does the orchestrator look like?
In the Python v2 programming model, it starts the run, races the event against a timer, and reads the run either way. current_utc_datetime keeps the deadline the same on every replay:
start_sume_run(instance_id)POSTs the Format run withIdempotency-Keyset to the instance id andcommunication.webhook_urlset to your webhook function plus?instance=<instance_id>, and returnsdata.id. It reads the base URL andSUME_API_KEYfrom app settings, which orchestrators must not read directly.read_sume_run(run_id)GETs/v1/format-runs/{run_id}and returnsstatus,primary_output_url, anderrorfrom the receipt.
import azure.functions as func
import azure.durable_functions as df
from datetime import timedelta
myApp = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS)
@myApp.orchestration_trigger(context_name="context")
def video_orchestrator(context: df.DurableOrchestrationContext):
run_id = yield context.call_activity("start_sume_run", context.instance_id)
deadline = context.current_utc_datetime + timedelta(minutes=100)
timeout_task = context.create_timer(deadline)
ended_task = context.wait_for_external_event("SumeRunEnded")
winner = yield context.task_any([ended_task, timeout_task])
if winner == ended_task:
timeout_task.cancel()
# Event or timeout (a canceled run never POSTs): read the receipt once.
return (yield context.call_activity("read_sume_run", run_id))How does the webhook function raise the event?
Sume signs <timestamp>.<raw_body> with HMAC-SHA256, so check the raw bytes from req.get_body() before parsing: refuse an empty secret, allow five minutes of clock skew, accept a match on any sume-v1= entry, and compare with hmac.compare_digest. Then raise the event on the instance named in the URL.
- Use the
anonymousauth level and rely on the signature. Sume's deliveries carry its own signature headers, not an Azure key, so a function key would have to ride in the URL'scodeparameter, and every receipt shows your URL aswebhook_delivery.url. - Don't give Sume the built-in
raiseEventURL from the Durable Functions HTTP API. It carries the system key, which grants access to all Durable Functions HTTP APIs, and nothing on that route checks Sume's signature. raise_eventraises an exception on a 404 or 400 response. Catch it and still answer2xx; the orchestrator's timer path covers that run.
import hashlib, hmac, json, os, time
@myApp.route(route="sume-webhook", methods=[func.HttpMethod.POST])
@myApp.durable_client_input(client_name="client")
async def sume_webhook(req: func.HttpRequest, client: df.DurableOrchestrationClient):
raw = req.get_body() # the exact bytes Sume signed
secret = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "").encode()
ts = req.headers.get("x-sume-webhook-timestamp", "")
if not secret or not ts.isdecimal() or abs(time.time() - int(ts)) > 300:
return func.HttpResponse(status_code=401)
expected = "sume-v1=" + hmac.new(secret, ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
entries = req.headers.get("x-sume-webhook-signature", "").split(",")
if not any([hmac.compare_digest(e.strip(), expected) for e in entries]):
return func.HttpResponse(status_code=401)
event = json.loads(raw)
try:
await client.raise_event(req.params.get("instance"), "SumeRunEnded", {"run_id": event["run_id"]})
except Exception:
pass # instance gone: its timer path reads the run
return func.HttpResponse(status_code=204)Do Durable Functions time out while they wait?
No. The wait-for-external-event API waits indefinitely, you can unload the function app while it waits, and on the Consumption plan no billing charges accrue while an orchestrator awaits an external event. What times out is each function execution: an activity must finish within the app's function timeout, by default 30 minutes on the Flex Consumption plan and 5 on the legacy Consumption plan. Starting a Sume run is one HTTPS call, so the video's minutes are spent waiting, not executing.
The timeout you choose is the timer's. A 100-minute timer outlasts Sume's 90-minute deadline, so the run has ended by the time read_sume_run looks. That read is also how you learn about runs that never POST: a canceled run delivers no webhook, and an endpoint that refuses all 10 attempts leaves the run unchanged with nothing delivered. Cancel the timer when the event wins; Durable Functions doesn't mark an orchestration Completed while a timer is still outstanding.
Sources
- Create a run
- Runs and results
- Run webhooks
- Webhooks
- Azure Durable Functions: External events (read 2026-09-27)
- Azure Durable Functions: Durable timers (read 2026-09-27)
- Azure Durable Functions: Orchestrator code constraints (read 2026-09-27)
- Azure Durable Functions: Bindings (read 2026-09-27)
- Azure Durable Functions: HTTP API reference (read 2026-09-27)
- Azure Durable Functions Python: DurableOrchestrationContext (read 2026-09-27)
- Azure Durable Functions Python: DurableOrchestrationClient (read 2026-09-27)
- Azure Functions: HTTP trigger (read 2026-09-27)
- Azure Functions: Hosting options (read 2026-09-27)
- Azure Functions: Configure function app settings (read 2026-09-27)
- Python: hmac (read 2026-09-27)
Related posts
More in Integrations
- Express raw body for webhook signatures and the 100kb limit
Mount express.raw with type application/json and a limit above 1 MiB on the Sume webhook route, then pass the raw Buffer to verifyWebhook.
- FastAPI long running task: return 202 with a job id
Don't hold a FastAPI request open for a long task: return 202 with a job id. For an AI video job, Sume does the work and a webhook brings the result.
- Gemini CLI MCP server: add Sume's hosted MCP
Add Sume's hosted MCP server to Gemini CLI with httpUrl and an API-key header read from your environment, then allowlist and confirm its tools.
- GitHub Actions: generate a release video with a Sume Format
Start a Sume Format run when a GitHub release is published, pass the notes as input, poll until the video is ready, and attach it to the release.
Written by Sume