Register an endpoint
Add the URL under Settings → Webhooks, or create it programmatically with POST /api/v1/webhooks. Pepys then posts to it whenever a job reaches a terminal state – no polling loop required.
The two events are transcription.completed and transcription.failed. The payload carries a summary of the job; fetch the full transcript from Get a transcription when you need the text and segments.
POST https://your-server.com/webhooks/pepys
Pepys-Event-Type: transcription.completed
Pepys-Event-Id: <jobId>.done
Pepys-Signature: t=1718800000,v1=<hmac-sha256-hex>
{
"id": "<jobId>.done",
"type": "transcription.completed",
"created": 1718800000,
"data": { "transcription": { "id": "…", "status": "done", "url": "https://pepys.co/api/v1/transcriptions/…" } }
}Verify the signature
Recompute HMAC-SHA256 over the timestamp, a literal dot, and the raw request body – {timestamp}.{rawBody} – using your endpoint's secret, then compare it to the v1 value from the header with a constant-time comparison. An optional replay window, rejecting deliveries whose timestamp is more than five minutes old, is a sensible extra check.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const ok = v1.length === expected.length &&
timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
if (!ok) throw new Error("bad signature");
// Optional: reject if Math.abs(Date.now()/1000 - Number(t)) > 300 (replay window).
return JSON.parse(rawBody);
}Retries and timeouts
Respond with a 2xx within 10 seconds. A non-2xx response or a timeout is retried with backoff, up to six attempts, so keep the handler fast: acknowledge the delivery, then do the slow work asynchronously on your side.