communication.webhook_url 400: HTTPS, public host, 2048 chars
A communication.webhook_url that is not public HTTPS, is over 2048 characters, or points at localhost or a private network returns 400 invalid_request.

A run create returns 400 invalid_request when communication.webhook_url is not a public HTTPS URL of at most 2048 characters. Sume rejects localhost, private-network addresses and non-HTTPS URLs. This is why a webhook that works against a tunnel on your laptop can fail the moment you point the create at http://localhost:3000/hook. Use an HTTPS tunnel with a public hostname for local work, or poll.
The field rules
The same shape works on Action runs, Format runs and Agent Completions.
| Field | Rule |
|---|---|
communication.webhook_url | Public HTTPS URL, at most 2048 characters |
communication.callback_url | Accepted alias for webhook_url. Send one or the other |
communication.mode | async (default) or webhook. The URL arms delivery; mode is descriptive |
Top-level webhook_url, callback_url, mode | fal-shaped aliases, normalized into communication.*. The same value on both layers is fine; values that disagree return 400 invalid_request |
One surprise comes from that last row. Sending webhook_url at the top level and a different communication.webhook_url is a 400, not a silent pick, so send the URL once.
Validation happens twice
Sume validates the URL again at delivery time, not only when you submit. If the URL no longer passes that check, the delivery fails re-validation and the receipt's webhook_delivery ends in failed or exhausted. Sume also does not follow redirects. A 3xx from your endpoint is not a delivery, so a route that redirects from http to https, or adds a trailing slash with a 301, loses every attempt. Register the final URL exactly as your server answers it.
A pre-flight check
The helper below catches the common mistakes before the create call. It is a convenience, not a copy of Sume's validator: Sume's check is the authority, and a URL that passes here can still be refused. It prints the result for a good URL, an http URL and a localhost URL.
// Cheap pre-flight for communication.webhook_url. Sume re-validates at delivery time.
export function webhookUrlProblem(raw) {
if (raw.length > 2048) return "longer than 2048 characters";
let u;
try { u = new URL(raw); } catch { return "not a URL"; }
if (u.protocol !== "https:") return "must be HTTPS";
const h = u.hostname;
if (h === "localhost" || h.endsWith(".local") || /^(10\.|127\.|192\.168\.|169\.254\.)/.test(h)) {
return "local or private host";
}
return null;
}
for (const url of ["https://hooks.example.com/sume", "http://hooks.example.com/sume", "https://localhost:3000/h"]) {
console.log(url, "->", webhookUrlProblem(url));
}When delivery is armed
- A URL on the create arms delivery on both
api.dev.sume.comandapi.sume.com. Until the run reaches a terminal state,webhook_delivery.statusisnot_armed. - One signed POST is sent when the run completes or fails. A canceled or skipped run never delivers.
- Each attempt has a 10 second timeout, and a run delivery has up to 10 attempts.
- If delivery ends in
failedorexhausted, read the receipt fromresult_url, fix the endpoint, and replay withPOST …/webhook/redeliver. - Keep polling
status_urlas a backup. A webhook only saves you the loop; it is not the only way to the result.
Local development
For a quick local loop, skip the webhook. Poll status_url and read result_url, or run waitForRun in a script. When you do need to test the receiver, expose it through an HTTPS tunnel that gives a public hostname, and verify signatures with your real signing secret so the code you test is the code you ship. The signing secret is readable on the Webhooks tab of the dashboard or from GET /v1/webhooks/signing-secret with a key that has account:read, and your receiver should refuse to start when it is empty. See Run webhooks for the field table and the delivery rules.
Sources
Related posts
More in Developers
- Connect a new MCP client to Sume: five calls that prove it works
After you add https://mcp.sume.com/mcp to a new client, run mcp_health, tools_list, tools_schema, account_me and catalog_list. What each result should show.
- Convert an SRT file to Sume caption cues in Python
Sume captions take no SRT upload, but cues carry the same start, end and text. A 26-line Python script turns an SRT into cues and posts them for $0.20.
- Cost per ad variant: build a ledger from usage.cost on Sume
Every completed /v1/videos poll carries usage.cost. Sum it by hook and ending to get the cost per ad variant before media spend. Node script and the caveats.
- Create an AI avatar and its first talking video in one bash script
Two Sume jobs in order: create the avatar, wait, then render a talking video with its handle. A bash script with curl and jq, plus the cost of both steps.
Written by Sume