Gareth Wilson Gareth Wilson

Guide to Argil Webhooks: Features and Best Practices

Published


Argil turns scripts into videos presented by AI avatars, and lets you train a custom avatar from your own footage. Both jobs take time, so Argil's webhooks tell you when a render or a training has finished, or failed, instead of making you poll the videos and avatars endpoints. A typical receiver downloads the finished video, marks a job as done in your own database, or tells a user that their new avatar is ready.

Below you'll find how Argil webhooks are registered through the API, what the four events contain, how to check the Standard Webhooks signature, and where Argil's delivery rules can trip you up.

What are Argil webhooks?

An Argil webhook is a callback URL plus a list of events, created with a call to the Argil API. There is no settings page for them: the webhook introduction describes list, create, update and delete calls only. A single webhook can subscribe to several events, so one endpoint can handle everything Argil sends.

Each delivery is a POST with a small JSON body, { "event": ..., "data": ... }, and three headers from the Standard Webhooks specification. The signing secret comes back in the response to the create call and nowhere else. Webhooks created before Argil added signing still arrive, but without any signature headers.

Argil webhook features

FeatureDetails
ConfigurationAPI only: GET and POST https://api.argil.ai/v1/webhooks, plus update and delete
Authentication for the APIx-api-key header
Subscription modelOne callback URL and a list of events per webhook
SignatureStandard Webhooks HMAC-SHA256
Signature headerwebhook-signature (v1, + base64)
Other headerswebhook-id (event ID, unchanged across retries), webhook-timestamp (Unix seconds of the attempt)
Secretwhsec_ + base64, returned only by the create call
BodyJSON {event, data}
Event routingBody field event
Response windowAny 2xx within 10 seconds. Redirects are not followed
Retries30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, then every 12 hours, for 3 days
OrderingAt least once, can arrive out of order
DisablingAnswer 410 Gone, or fail for 3 days (the workspace owner is emailed)
Source IPsNot published

Events and payloads

EventTriggered WhenCommon Use Cases
VIDEO_GENERATION_SUCCESSAn avatar video render completesDownload or publish videoUrl, mark the job done
VIDEO_GENERATION_FAILEDA render failsAlert, retry, check whether the credit debit was refunded
AVATAR_TRAINING_SUCCESSAn avatar training completesEnable the avatar (avatarId, voiceId) in your app
AVATAR_TRAINING_FAILEDAn avatar training failsNotify the user, ask for new source footage

Argil's docs list these four and don't say the list is complete, so return 200 for any event name you don't recognise.

Video events

A successful render sends the video's ID, name and public URL, plus the credits it cost:

{
  "event": "VIDEO_GENERATION_SUCCESS",
  "data": {
    "videoId": "<video_id>",
    "videoName": "<video_name>",
    "videoUrl": "<public_url_to_access_video>",
    "message": "Video generation completed successfully",
    "extras": "<additional_key-value_data_related_to_the_video>",
    "renderCost": 150,
    "renderCostRefunded": false
  }
}

VIDEO_GENERATION_FAILED has the same shape without videoUrl, and message explains why the render failed. renderCost is the number of credits debited, or 0 for a free render. Argil refunds a failed render before it sends the webhook, so renderCostRefunded is usually true on a failure. If the refund itself failed, the flag is false and the charge stands, which is worth surfacing to whoever handles billing.

Avatar events

AVATAR_TRAINING_SUCCESS carries avatarId, voiceId, avatarName and extras. AVATAR_TRAINING_FAILED carries the same fields without voiceId. The failure page's prose calls the event AVATAR_GENERATION_FAILED, but its example, the subscription enum and the introduction all use AVATAR_TRAINING_FAILED, so match on the latter.

The examples in Argil's docs use <placeholder> strings rather than real values, and extras is described only as additional key-value data, so check its type against a real delivery before you rely on it.

Setting up Argil webhooks

You need an Argil API key and a public HTTPS URL. Create the webhook with the events you want:

curl -X POST https://api.argil.ai/v1/webhooks \
  -H "x-api-key: $ARGIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"callbackUrl": "https://your-app.com/webhooks/argil",
       "events": ["VIDEO_GENERATION_SUCCESS", "VIDEO_GENERATION_FAILED",
                  "AVATAR_TRAINING_SUCCESS", "AVATAR_TRAINING_FAILED"]}'

callbackUrl and at least one entry in events are required. The 201 response holds the webhook's id, callbackUrl, events, createdAt and updatedAt, and a secret such as whsec_C2FVsBQIhrscChlQIMV+b5sSYspob7oD. Save that secret as ARGIL_WEBHOOK_SECRET straight away. Argil documents no call that returns it again, so if it is lost you delete the webhook and create a new one.

ActionCall
ListGET https://api.argil.ai/v1/webhooks (reference)
CreatePOST https://api.argil.ai/v1/webhooks (reference)
Update (URL or events, re-enables a disabled webhook)reference
Deletereference

To try it locally, forward deliveries to your machine with the Hookdeck CLI and register the URL it prints:

npx hookdeck-cli listen 3000 argil --path /webhooks/argil

Then render a short test video and watch for VIDEO_GENERATION_SUCCESS.

With Hookdeck. Create a source with the Argil type, paste the full whsec_ secret, and use the source URL as callbackUrl. Webhooks created before Argil added signing are unsigned. Recreate the webhook to get a whsec_ secret. Requests with any method other than POST are refused. When a request fails the signature check, Hookdeck still answers Argil with a 200, records the request as failed verification and does not forward it.

Securing Argil webhooks

Argil signs {webhook-id}.{webhook-timestamp}.{raw body} with HMAC-SHA256 and sends the result as v1,<base64> in webhook-signature. The key is the part of the secret after whsec_, base64-decoded. The header can hold several space-separated signatures, and a match on any v1 entry is enough.

const crypto = require('node:crypto');

function decodeBase64(value) {
  if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
  const bytes = Buffer.from(value, 'base64');
  return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
    ? bytes : null;
}

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const prefix = headers['webhook-id'] !== undefined ? 'webhook' : 'svix';
  const id = headers[`${prefix}-id`];
  const timestamp = headers[`${prefix}-timestamp`];
  const signature = headers[`${prefix}-signature`];
  if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
      !/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
  const key = decodeBase64(secret.replace(/^whsec_/, ''));
  if (!key) return false;
  const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
  const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    const received = decodeBase64(candidate);
    if (!received) return false;
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

Wire it into an Express route that keeps the raw body. Because Argil retries with the same webhook-id, that header makes a natural idempotency key:

const express = require('express');

// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
  try {
    const value = JSON.parse(rawBody.toString('utf8'));
    return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
  } catch {
    return null;
  }
}

const app = express();

app.post('/webhooks/argil', express.raw({ type: '*/*' }), async (req, res) => {
  const secret = process.env.ARGIL_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500);
  if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

  const payload = parseJsonObject(req.body);
  if (!payload) return res.sendStatus(400); // signed, but not a JSON object
  const { event, data } = payload;
  if (typeof event !== 'string' || !data || typeof data !== 'object') return res.sendStatus(400);
  // Store req.headers['webhook-id'] first and skip IDs you've already seen.
  try {
    if (event === 'VIDEO_GENERATION_SUCCESS') {
      await queueVideoDownload(data.videoId, data.videoUrl);
    } else if (event === 'VIDEO_GENERATION_FAILED' && !data.renderCostRefunded) {
      await flagUnrefundedRender(data.videoId, data.renderCost);
    }
  } catch {
    return res.sendStatus(503); // not processed, so don't acknowledge
  }
  return res.sendStatus(200);
});

queueVideoDownload and flagUnrefundedRender stand in for your own code. Keep them quick, or hand the work to a queue, because Argil gives up on an attempt after 10 seconds.

Argil's own example uses the standardwebhooks package. That library refuses any webhook-timestamp more than 5 minutes from your server's clock. Hookdeck sets no timestamp limit and keeps Argil's original headers when it retries or replays a request, so a delivery that waited in Hookdeck can fail the library's check even though it is genuine. The function above checks the signature only. If you want a freshness check on direct traffic, add one with a window longer than any delay you expect.

Make Argil webhooks production-ready. Hookdeck Event Gateway checks Argil's Standard Webhooks signature, keeps every render and training result, and lets you replay one when your handler had a bad day.

Argil webhook limitations and pain points

Legacy webhooks are unsigned

The Problem: An older webhook keeps delivering events, but none of them carries a signature.

Why It Happens: Argil added signing later. Webhooks created before then are still delivered without signature headers, and Argil's docs describe no way to add signing to an existing webhook.

Workarounds:

  • List your webhooks, delete any created before signing, and create them again to receive a secret.
  • Until you do, treat those deliveries as unverified notifications and fetch the video or avatar from the API before acting.

How Hookdeck Can Help: Once the Argil source has a secret, Hookdeck marks unsigned requests as failed verification, so an old webhook pointed at it shows up quickly.

The secret is shown once

The Problem: You've lost the signing secret and can't verify anything.

Why It Happens: Only the create response contains secret. The list and update calls don't return it, and Argil documents no rotate call.

Workarounds:

  • Write the secret to your secret manager in the same script that creates the webhook.
  • If it's gone, delete the webhook, create a new one and update your configuration.

How Hookdeck Can Help: With Hookdeck in front, the secret lives on the source, so replacing it is one change rather than a redeploy of every receiver.

Ten seconds and no redirects

The Problem: Deliveries keep being retried even though your handler eventually succeeds.

Why It Happens: Argil counts anything slower than 10 seconds as a failure and doesn't follow redirects, so a 301 from a moved route or an HTTP-to-HTTPS redirect is a failure too.

Workarounds:

  • Register the final HTTPS URL, not one that redirects.
  • Acknowledge first and download videos or update records afterwards.

How Hookdeck Can Help: Hookdeck answers Argil immediately and delivers to your service with its own timeout and retry policy.

Three days, then disabled

The Problem: After a long outage, Argil stops sending events altogether.

Why It Happens: Argil retries for 3 days and then disables the webhook and emails the workspace owner. Returning 410 Gone also disables it at once, so a router that answers 410 for an unknown path can switch the webhook off by accident.

Workarounds:

  • Never return 410 from the webhook route unless you mean to unsubscribe.
  • After an incident, list your webhooks and update any that were disabled. An update re-enables it.

How Hookdeck Can Help: Hookdeck accepts each delivery as it arrives, so your downtime doesn't count against Argil's 3 days, and it keeps the events until your service is back.

Duplicates and out-of-order events

The Problem: A render is processed twice, or a failure arrives after you've already marked the video as done.

Why It Happens: Argil delivers at least once and makes no ordering promise.

Workarounds:

  • Deduplicate on webhook-id.
  • Store the latest state per videoId or avatarId, and fetch the video from the API when two events disagree.

How Hookdeck Can Help: Hookdeck's deduplication can drop repeated webhook-id values before they reach you.

Best practices

Keep the body raw until it's verified

The signature covers the exact bytes Argil sent. Mount express.raw() on the route, use await request.text() in a Next.js route handler, or await request.body() in FastAPI, and parse only after the check passes.

Watch renderCostRefunded

A failed render with renderCostRefunded: false means credits were spent and not returned. Route those to someone who can raise it with Argil rather than retrying the render silently.

Don't assume the one-shot callback is signed

POST /videos/{id}/render accepts its own callbackUrl, which receives the same video fields. Argil's docs don't say whether those calls are signed, so check a real request before you send them through the same verifier, or rely on registered webhooks instead.

Process asynchronously

Download large video files in a background job. See why to process webhooks asynchronously and our guide to webhook idempotency.

Conclusion

Argil's webhooks are few and simple: four events about renders and avatar trainings, created through the API and signed per Standard Webhooks. The details that matter are operational. Save the secret when you create the webhook, recreate any legacy unsigned ones, answer within 10 seconds without redirects, and never let your route return 410 by accident.

Hookdeck Event Gateway can sit between Argil and your service, verify each delivery and hold it through your outages, so a long incident doesn't end with a disabled webhook. Sign up for Hookdeck to try it with your Argil account.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.