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.

6 min readSume
All posts

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.

From Microsoft Learn's external events, timers, code constraints, and Python client pages and Sume's Create a run, Runs and results, and Run webhooks pages, read 2026-09-27.
PieceDurable Functions ruleSume side
start_sume_run activityOutbound 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_anyCancel 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_eventEvents 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 with Idempotency-Key set to the instance id and communication.webhook_url set to your webhook function plus ?instance=<instance_id>, and returns data.id. It reads the base URL and SUME_API_KEY from app settings, which orchestrators must not read directly.
  • read_sume_run(run_id) GETs /v1/format-runs/{run_id} and returns status, primary_output_url, and error from 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 anonymous auth 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's code parameter, and every receipt shows your URL as webhook_delivery.url.
  • Don't give Sume the built-in raiseEvent URL 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_event raises an exception on a 404 or 400 response. Catch it and still answer 2xx; 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

Related posts

More in Integrations

All Integrations posts

Written by Sume