Logo of wyzie-docsWyzie Docs

Wyzie Subs

Webhooks#

Webhooks are free and optional. Send a webhook URL with a Transcribe, Fix Subtitles or bulk request and we call it when the job ends, so you do not have to poll. A webhook never carries your key or the subtitle.

Without a webhook you poll the job link until it is done. With one, we send a signed JSON POST to your server when the job finishes or fails, and you fetch the result with your own key.

Add one#

Add webhook=https://example.com/wyzie as a form field or query parameter on POST /transcribe and POST /fix, or as a JSON field on POST /transcribe/batch.

curl -X POST "https://sub.wyzie.io/transcribe?key=YOUR_KEY&webhook=https://example.com/wyzie" \
  -H "Content-Type: audio/mpeg" --data-binary @podcast.mp3

The URL must be https on the default port, point to a public address and not redirect. We wait 5 seconds for your answer, and any 2xx counts as delivered. A URL we cannot use is refused with 400 before anything is charged.

A batch sends one batch.done call when every item has ended. Add webhookEach: true to also get one call per item, as transcribe.done or transcribe.failed with the batch id and an index.

Events#

The event is in the payload and in the x-wyzie-event header.

EventMeaning
transcribe.done, fix.doneThe job finished. Fetch the subtitle from result.
transcribe.failed, fix.failedThe job failed. error says why. A failure on our side was refunded.
batch.doneEvery item of a bulk job has ended. counts shows how many are done, failed or skipped.

Payload#

The body is JSON. result is the poll link, to be fetched with your key. expiresAt is when we delete the job: one hour after it ends, 24 hours for a batch. charged is what was actually taken, cost the price. error is only on a failure.

{
  "event": "transcribe.done",
  "id": "JOB_ID",
  "status": "done",
  "cost": 6000,
  "charged": 6000,
  "duration": 1800,
  "language": "en",
  "result": "https://sub.wyzie.io/transcribe/JOB_ID",
  "expiresAt": "2026-11-03T13:00:00.000Z"
}

Check the signature#

Each key has one signing secret, whsec_..., that does not change. It comes back as webhookSecret in the JSON of the 202 answer (or an error answer), and as the x-wyzie-webhook-secret header when the result is returned straight away. Store it on your server. It is not your API key, and anyone who holds it can only check our signatures.

Every call has x-wyzie-signature: t=<unix time>,v1=<hex>, where v1 is the HMAC-SHA256 of t.body (the timestamp, a dot, the raw body) with your secret. Check it against the raw body, compare in constant time, and reject a t that is too old.

import { createHmac, timingSafeEqual } from "node:crypto";
 
function verify(secret, header, rawBody) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const want = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const a = Buffer.from(want);
  const b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

Delivery and retries#

Delivery is at least once. x-wyzie-delivery is the same on every retry of one event, so use it to ignore repeats.

If your server does not answer with a 2xx, we retry after 30 seconds, 5 minutes and 30 minutes, then give up (4 attempts in all). A key can have at most 200 undelivered webhooks at a time. A failed webhook never changes billing or the result.

The poll answer shows the state: a webhook object with status (pending, delivered or failed) and attempts, or the headers x-wyzie-webhook and x-wyzie-webhook-attempts when it returns the subtitle.

Privacy#

Your webhook URL and the payloads queued for it are kept in Redis until they are delivered or given up, at most a few hours. They hold no key and no subtitle.