Skip to main content

Overview

Every create endpoint, and POST /v1/render, takes an optional webhook URL. When the video finishes, or the project fails, Hooked sends a signed POST with JSON to that URL (Content-Type: application/json). The URL you pass on create belongs to that project and is called for its automatic render. A render started later with POST /v1/render calls only the webhook passed in that request, and a render started from the dashboard editor calls none. You can also register team endpoints in the dashboard under Settings → Webhooks. Each one picks the events it wants and receives them for every project and render in the team, whether it was started from the API or the dashboard. If you pass no webhook and register no endpoint, nothing is sent and you can poll GET /v1/project/{projectId} instead.

How it works

1

Pass a webhook URL

2

Hooked generates the video

The create call answers right away with a projectId. Generation takes a few minutes depending on the format.
3

Hooked calls your URL

Once the video is rendered, or when the project fails, your URL gets a POST with the result.
4

Verify the signature, then answer 2xx quickly

Respond with any 2xx status within 10 seconds. Do the slow work (downloading, emailing) after you answer.

URL requirements

  • https:// only. Anything else is rejected at create time with 400 webhook: Must be a valid HTTPS URL.
  • A public host. A URL that names localhost, a private IP, a *.local/*.internal host or a cloud metadata address is refused at create time with the same 400. A hostname that only resolves to a private address is accepted on create, but the call is never sent.
  • Up to 500 characters.
  • Redirects are not followed. A 3xx counts as a failed attempt, so give the final URL.

Payloads

The body has no event field (it is the same body Hooked has always sent). Read the top-level status, COMPLETED or FAILED, or the Hooked-Event header.

Headers and events

Every call carries these headers: The per-request webhook URL of a project gets the first three; the one of a media call gets the media.* pair (see Media webhooks). A team endpoint gets only the events it subscribed to.

Video completed

Render failed or timed out

message is Video rendering failed, Video rendering timed out or Video failed.

Project failed before it had a video

If the project fails while it is still generating (a provider error, running out of credits mid-way), there is no video yet, so videoId is null:
message is the project’s own failure message. Credits charged for a failed project are refunded automatically.
Some failures while generating do not send this call yet (for example, a narrated video whose AI images or clips failed). Always poll GET /v1/project/{projectId} as a backup when no webhook arrives.

Fields

string
COMPLETED or FAILED.
string
A human-readable summary. On failures, the reason.
object

Media webhooks

POST /v1/media/generate/image, POST /v1/media/generate/video and POST /v1/media/import also take an optional webhook (same URL requirements) and metadata (any JSON object, up to 5 KB). The URL is told once, when the media becomes COMPLETED or FAILED, with the event media.completed or media.failed, signed with the same request signing secret as project webhooks. Team endpoints subscribed to media.completed / media.failed get the same body for every library generation, import and upload of the team, from the API or the dashboard.
  • url is the file, signed and expiring like every Hooked link; "" when FAILED. Get Media gives a fresh one.
  • type is image or video.
  • error is why it failed (null when COMPLETED); Get Media shows the same text. A failed generation’s credits are refunded automatically.
  • metadata is the object you sent, or null.
A media still running after 60 minutes is given up on: it becomes FAILED and media.failed is sent then. Poll Get Media as a backup when no webhook arrives.

Retries

Hooked tries each delivery up to 3 times in a row, with no delay between attempts. An attempt fails when your endpoint answers anything other than 2xx, does not answer within 10 seconds, or the connection fails. After the third failed attempt the notification is dropped. Because of that:
  • Answer fast and do the work afterwards.
  • Do not rely on the webhook alone. If your endpoint was down, poll GET /v1/project/{projectId} to catch up.
  • Handle duplicates. If your endpoint processed a call but answered too late, the same payload arrives again with the same Hooked-Delivery id. Use that id, or videoId (projectId when videoId is null), to ignore repeats.

Verify the signature

Every call is signed with HMAC-SHA256, so you can tell that it came from Hooked and was not changed on the way.

Where to find the secret

In the dashboard, open Settings → Webhooks:
  • Signing secret signs the calls to the webhook URL you pass in API requests. There is one per team, created the first time it is needed.
  • Each team endpoint has its own secret, shown on its row.
Both start with whsec_. Click the eye icon to reveal one. Rotate creates a new secret at once and the old one stops working, so update your server right after.

How the signature is built

v1 is the hex HMAC-SHA256 of "<t>.<raw body>" using the secret as the key. To check it:
  1. Read t and v1 from the header.
  2. Reject the call if t is more than 5 minutes away from your clock (stops replays).
  3. Compute the HMAC over t, a ., and the raw request body exactly as received. Do not parse and re-serialize the JSON first: any change breaks the signature.
  4. Compare it with v1 in constant time.

Node.js

Python

Extra checks

  • A token in the URL (?token=...) is no longer needed, but you can still add one as a second check.
  • Do not trust the payload blindly for sensitive actions: confirm it with GET /v1/project/{projectId} using your API key. This also gives you a fresh download link.
  • Use your own correlation ids in metadata so you know which of your records a call belongs to.

Implementing an endpoint

Node.js (Express)

Python (FastAPI)

Next.js route handler

Testing

Local development

Hooked can only reach public HTTPS URLs, so expose your local server with a tunnel such as ngrok:
You can also point the webhook at a request inspector like webhook.site to see the exact payloads.

Send yourself a test payload

Sign a sample body with your secret the same way Hooked does:

Common issues

  • Check the URL is https://, public, and does not redirect.
  • Make sure your endpoint answers 2xx within 10 seconds.
  • Check your server logs for three quick attempts close together: that means it answered with an error.
  • Poll GET /v1/project/{projectId}: if the project is still processing, there is nothing to send yet.
  • Verify the raw body. Frameworks that parse JSON first (for example express.json()) change the bytes.
  • Use the right secret: the Signing secret for the webhook URL in your requests, the endpoint’s own secret for a team endpoint.
  • After a Rotate, the old secret stops working at once.
  • Check your server clock: calls more than 5 minutes off are rejected by the snippets above.
Your endpoint answered late or with an error and Hooked retried. Deduplicate on videoId / projectId.

Next steps

Error Handling

Handle API errors

Best Practices

Production-ready patterns