Test your speech integration without spending quota

Mock the API in unit tests, and run one tiny live smoke test that reads the key from the environment and never prints it.

Two layers: fast tests with a fake API, and one live check you run on demand.

1. Unit test with a fake fetch (Node 18+)

say.mjs

export async function say(text, fetchImpl = fetch, key = process.env.SPEAKVORA_KEY) {
  const r = await fetchImpl("https://speakvora.com/api/v1/speak", {
    method: "POST",
    headers: { "x-api-key": key, "content-type": "application/json" },
    body: JSON.stringify({ text, voice: "af_heart", lang: "en" }),
  });
  const j = await r.json();
  if (!r.ok) throw new Error(j.error);
  return j.url;
}

say.test.mjs

import test from "node:test";
import assert from "node:assert/strict";
import { say } from "./say.mjs";

test("returns the audio url", async () => {
  const fake = async () => ({ ok: true, json: async () => ({ url: "https://example.test/a.m4a" }) });
  assert.equal(await say("Hello.", fake, "test-key"), "https://example.test/a.m4a");
});

test("throws the API error code", async () => {
  const fake = async () => ({ ok: false, json: async () => ({ error: "free_allowance_used" }) });
  await assert.rejects(() => say("Hello.", fake, "test-key"), /free_allowance_used/);
});

Run

node --test

2. One live smoke test

smoke.mjs

import { say } from "./say.mjs";
if (!process.env.SPEAKVORA_KEY) { console.error("Set SPEAKVORA_KEY first"); process.exit(1); }
const url = await say("Smoke test.");
const head = await fetch(url, { method: "HEAD" });
console.log(head.ok ? "ok: audio link reachable" : "FAIL: link returned " + head.status);

Run it only when you change the integration. It uses a few characters of your free allowance and never prints the key. In CI, store the key as a secret and run the smoke test on a schedule, not on every commit.