- Subscribe to both terminal events when you need both outcomes.
- Verify raw bytes and record the event before acknowledging it.
- Deduplicate retries without discarding a newer version.
A create request returns HTTP 202. Your application knows the job exists, but it still cannot tell a customer that their file is ready. Let the job finish asynchronously. Give the receiver a durable record of the original request, the expected account and the destination.
Choose how the event reaches you
For one job, include webhook_url and webhook_secret in the create request. For all matching jobs on one ngram account, POST to /api/v1/webhooks/subscriptions with event_type and target_url. Register separate subscriptions for video.completed and video.failed. Save each subscription’s secret at creation; it is not returned on a later read. Use a public HTTPS receiver for production.
POST /api/v1/webhooks/subscriptions
Authorization: Bearer <server-side API key>
Content-Type: application/json
{
"event_type": "video.completed",
"target_url": "https://events.example.com/ngram/completed"
}
Create the failure subscription with event_type set to video.failed. A separate receiver path for each subscription makes it straightforward to select the matching signing secret before reading the body.
Start from the API getting-started guide to authenticate and check account access. Key creation is self-service. Creating a video with an API key requires Basic or a higher paid plan and sufficient credits.
Verify the signature before parsing the JSON
Keep the exact bytes delivered to the endpoint. Read X-Ngram-Timestamp and X-Ngram-Signature. Using the secret assigned to this callback or subscription, compute HMAC-SHA256 over the timestamp, a period and the raw bytes. Reject invalid signatures and timestamps more than five minutes from the server clock. The helper below assumes a Node request handler has already captured the raw body as a Buffer.
import { createHmac, timingSafeEqual } from "node:crypto";
function validWebhook(rawBody, headers, secret) {
const timestamp = headers["x-ngram-timestamp"];
const signature = headers["x-ngram-signature"];
if (typeof timestamp !== "string" || !/^\d+$/.test(timestamp)) return false;
if (typeof signature !== "string" || !/^sha256=[0-9a-f]{64}$/.test(signature)) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = createHmac("sha256", secret)
.update(timestamp + ".").update(rawBody).digest();
const received = Buffer.from(signature.slice(7), "hex");
return timingSafeEqual(expected, received);
}
Only after verification should the handler parse event, video_id and any version_id. Resolve the destination from your own job-to-record mapping, not from a free-form URL supplied by a customer. The webhook integration describes callback and subscription scopes.
Route a completed result to the correct record
In the completed example, video_id is vid_release_brief and the receiver gets a version_id, a result URL and duration_ms of 13200. An endpoint returning 204 has acknowledged receipt. Record delivery to the customer only after your application has checked the intended destination.
A preview-only job can complete without result.url. Its job-specific callback receives the event; account-wide subscriptions do not receive that preview completion. Read live_preview on GET /api/v1/videos/{id}, or export the approved version if the recipient needs an MP4.
Send failure to the owner, not the customer’s file slot
The failure example has video_id vid_source_review, error_code generation_failed and the message “Could not finish the video.” Keep the failure and its job ID with the source record. Notify the operator who can inspect the cause. Do not create a file placeholder that looks like a successful video.
Acknowledge durable receipt and make retries safe
Persist the event or enqueue it durably, with a unique processing key, before answering 2xx. Keep that acknowledgement within ten seconds. A timeout or non-2xx response can lead to retries after 1, 2, 5, 15 and 30 minutes, for up to six attempts in total. HTTP 410 tells a persistent subscription that the receiver is gone and removes the subscription.
For a versioned completion, key the delivery on video_id and version_id. The same pair should not notify twice. A new edit can complete under the same video_id with a new version_id; process it as a new delivery when appropriate. Use a separate failure key and read current status when an older completion has no version_id. The public API integration covers the job and export reads you can use to reconcile a result.
You just read it. Now watch it.
ngram turns this post into a short explainer video: scenes, voiceover, and motion graphics included.






