Voice governance
Rules for what your voice agents may say, in which voice, and with whose permission. They apply to every call, whatever framework the agent uses. To connect an agent, see Govern your voice agent.
Voice governance is available on Pro and Enterprise. Changing the policy or registry needs the admin role; viewers can read them.
Disclosure, marking and consent are recorded in the audit log as evidence of what happened. Whether a setting satisfies a particular law, such as the EU AI Act's transparency duties, is for your counsel to decide.
Disclosure
A Disclosure tells the listener, at the start of a Voice Conversation, that they're talking to an AI. The policy sets what happens when the first Speech Synthesis of a conversation doesn't contain it.
| Mode | What happens |
|---|---|
log (default) | The call goes through. The audit entry records whether the Disclosure was present or missing. With no phrases and no inject text configured, nothing is checked and the entry records off. |
enforce | A first Speech Synthesis without the Disclosure is refused with 422 and code disclosure_missing. Nothing is synthesized or charged. With no phrases configured, every first Speech Synthesis is refused, so list at least one. |
inject | Forgebench prepends the workspace's inject_text for the conversation's language to the first Speech Synthesis. The audit entry records injected. If no inject text is set for the language or the default language, the call is refused as under enforce. |
Phrase matching. The Disclosure is present when the first Speech Synthesis
contains one of the workspace's phrases for the conversation's language, such
as "I'm an AI assistant". You set phrases and inject text per language.
default_language applies when a request doesn't specify one.
Which call is first. The session key (metadata.session or the
X-Forgebench-Session header) identifies the conversation. Send one on every
voice call.
- Later Speech Synthesis calls in the conversation are audited as
not_firstand aren't checked. - A call with no session key is always treated as first.
- The first-call marker lasts 24 hours.
- Conversations are tracked per agent, or per API key when there's no agent.
- A call that fails before it's served doesn't use up the first slot. That
includes
enforcerefusals, budget, plan and rate-limit refusals, guardrail blocks and provider errors. - Session keys are at most 128 characters. Longer keys get
422invalid_session, or a protocol error and close 1008 on streaming sockets.
Per-agent override. An agent's voice settings can set its own mode
(config.voice.disclosure_mode: log, enforce or inject), overriding the
workspace policy.
from forgebench import Forgebench, PermissionDeniedError, ValidationError
client = Forgebench(api_key="sk_...", base_url="https://api.forgebench.ai")
try:
client.voice.policy.update(
disclosure_mode="inject",
disclosure_phrases={"en": ["i'm an ai assistant", "i am an ai"]},
inject_text={"en": "Hi, I'm an AI assistant."},
default_language="en",
)
print(client.voice.policy.get()["disclosure_mode"])
except PermissionDeniedError as e:
print("admin role required:", e.code)
except ValidationError as e:
print("rejected:", e.code)import { Forgebench, PermissionDeniedError, UnprocessableEntityError } from "@seedlinglabs/forgebench-sdk";
const forgebench = new Forgebench({ baseUrl: "https://api.forgebench.ai", apiKey: "sk_..." });
try {
await forgebench.voice.policy.update({
disclosure_mode: "inject",
disclosure_phrases: { en: ["i'm an ai assistant", "i am an ai"] },
inject_text: { en: "Hi, I'm an AI assistant." },
default_language: "en",
});
console.log((await forgebench.voice.policy.get()).disclosure_mode);
} catch (e) {
if (e instanceof PermissionDeniedError) console.log("admin role required:", e.code);
else if (e instanceof UnprocessableEntityError) console.log("rejected:", e.code);
else throw e;
}import ai.forgebench.Forgebench;
import ai.forgebench.errors.PermissionDeniedException;
import ai.forgebench.errors.ValidationException;
import java.util.List;
import java.util.Map;
Forgebench client = Forgebench.builder()
.apiKey("sk_...")
.baseUrl("https://api.forgebench.ai")
.build();
try {
client.voice().policy().update(Map.of(
"disclosure_mode", "inject",
"disclosure_phrases", Map.of("en", List.of("i'm an ai assistant", "i am an ai")),
"inject_text", Map.of("en", "Hi, I'm an AI assistant."),
"default_language", "en"));
System.out.println(client.voice().policy().get().get("disclosure_mode"));
} catch (PermissionDeniedException e) {
System.out.println("admin role required: " + e.getCode());
} catch (ValidationException e) {
System.out.println("rejected: " + e.getCode());
}# PUT replaces the whole policy; fields you omit take their defaults
curl -X PUT https://api.forgebench.ai/v1/voice/policy \
-H "Authorization: Bearer $FORGEBENCH_API_KEY" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{
"disclosure_mode": "inject",
"disclosure_phrases": {"en": ["i'm an ai assistant", "i am an ai"]},
"inject_text": {"en": "Hi, I'm an AI assistant."},
"default_language": "en"
}
EOF
curl https://api.forgebench.ai/v1/voice/policy \
-H "Authorization: Bearer $FORGEBENCH_API_KEY"Recording text on traces
voice_trace_content (default false) controls whether text is recorded on
traces. It covers both Forgebench's transcripts and Speech Synthesis input, and
the text in telemetry your LiveKit or Pipecat pipeline sends. While it's off, no
transcript or speech text is recorded. Turn it on in Settings → Voice
(Record text on traces) or with voice.policy.update(voice_trace_content=True).
Secrets are always removed from recorded text. See
Observe your voice agent.
Marking
Some Speech Synthesis providers embed a mark in their output identifying it as machine generated. A workspace can require models that do this. Forgebench doesn't watermark audio itself.
Each speech model has a marking level:
| Level | Meaning |
|---|---|
confirmed | The provider documents that its output carries an identifiable mark. |
claimed | The provider says its output is marked, without documentation detailed enough to confirm it. |
none | No marking is documented. |
The setting is off by default. Turn on marking_required and set
marking_min_level to claimed (accepts confirmed and claimed) or confirmed
(accepts confirmed only). A Speech Synthesis on a model below the level is
refused with 403 and code marking_required.
Levels as of the last review, 2026-10-09:
| Provider and model | Level |
|---|---|
| Gemini TTS | confirmed |
| OpenAI speech models | claimed |
| ElevenLabs | claimed |
| Deepgram Aura | none |
| Google Chirp 3 HD | none |
Provider behaviour changes. The catalog shows each model's current level and review date.
Voice Assets and Consent Records
A Voice Asset is a named synthetic voice your agents may use, with its source and permitted uses. A Consent Record is timestamped evidence that a person agreed to a specific use, such as having their voice cloned or being recorded.
| Voice Asset kind | Rule |
|---|---|
stock | A provider's own voice. Open to all agents by default. |
brand | A voice your company owns. Must reference a valid (unrevoked, unexpired) Consent Record, or registration is refused with 422 consent_required. Every use re-checks the consent. |
cloned | A clone of a person's voice. Same rule as brand. |
voice_assets_required_for sets which voices must be registered, active and
allowed for the calling agent:
cloned_brand(default): a registered voice must be active, unexpired, allowed for the agent and, if cloned or brand, still consented. An unregistered voice is treated as stock and is open, so register every cloned or brand voice, or useall. Registered voices are matched on provider and voice id when the provider can be derived from the model.all: every voice must be a registered Voice Asset.none: the registry is not enforced.
A Speech Synthesis with a voice the policy doesn't allow is refused with 403
voice_not_allowed.
- Limit a Voice Asset to specific agents with
allowed_agent_ids, and give it an expiry. licensed_usesis for your records only; calls aren't checked against it.- A revoked Voice Asset is no longer active.
- Revoking a Consent Record stops every cloned voice that depends on it.
- Registering a provider voice twice returns
409voice_asset_exists.
from forgebench import ConflictError, ValidationError
try:
consent = client.voice.consents.create(
subject_name="Jane Rivera",
purpose="voice_clone",
scope="Support agent voice, English, until revoked",
evidence_uri="https://contracts.example.com/jr-2026-10",
)
asset = client.voice.assets.create(
provider="elevenlabs",
provider_voice_id="v_123",
display_name="Jane (support)",
kind="cloned",
consent_record_id=consent["id"],
)
# Withdraw later:
client.voice.assets.revoke(asset["id"])
client.voice.consents.revoke(consent["id"])
except ConflictError:
print("that provider voice is already registered")
except ValidationError as e:
print("rejected:", e.code)import { ConflictError, UnprocessableEntityError } from "@seedlinglabs/forgebench-sdk";
try {
const consent = await forgebench.voice.consents.create({
subject_name: "Jane Rivera",
purpose: "voice_clone",
scope: "Support agent voice, English, until revoked",
evidence_uri: "https://contracts.example.com/jr-2026-10",
});
const asset = await forgebench.voice.assets.create({
provider: "elevenlabs",
provider_voice_id: "v_123",
display_name: "Jane (support)",
kind: "cloned",
consent_record_id: consent.id,
});
// Withdraw later:
await forgebench.voice.assets.revoke(asset.id);
await forgebench.voice.consents.revoke(consent.id);
} catch (e) {
if (e instanceof ConflictError) console.log("that provider voice is already registered");
else if (e instanceof UnprocessableEntityError) console.log("rejected:", e.code);
else throw e;
}import ai.forgebench.errors.ConflictException;
import ai.forgebench.errors.ValidationException;
import java.util.Map;
try {
Map<String, Object> consent = client.voice().consents().create(Map.of(
"subject_name", "Jane Rivera",
"purpose", "voice_clone",
"scope", "Support agent voice, English, until revoked",
"evidence_uri", "https://contracts.example.com/jr-2026-10"));
Map<String, Object> asset = client.voice().assets().create(Map.of(
"provider", "elevenlabs",
"provider_voice_id", "v_123",
"display_name", "Jane (support)",
"kind", "cloned",
"consent_record_id", consent.get("id")));
// Withdraw later:
client.voice().assets().revoke((String) asset.get("id"));
client.voice().consents().revoke((String) consent.get("id"));
} catch (ConflictException e) {
System.out.println("that provider voice is already registered");
} catch (ValidationException e) {
System.out.println("rejected: " + e.getCode());
}# Record the consent; the response's "id" is the consent_record_id below
curl -X POST https://api.forgebench.ai/v1/voice/consents \
-H "Authorization: Bearer $FORGEBENCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject_name": "Jane Rivera",
"purpose": "voice_clone",
"scope": "Support agent voice, English, until revoked",
"evidence_uri": "https://contracts.example.com/jr-2026-10"
}'
# Register the voice (409 voice_asset_exists if it is already registered)
curl -X POST https://api.forgebench.ai/v1/voice/assets \
-H "Authorization: Bearer $FORGEBENCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "elevenlabs",
"provider_voice_id": "v_123",
"display_name": "Jane (support)",
"kind": "cloned",
"consent_record_id": "'"$CONSENT_ID"'"
}'
# Withdraw later
curl -X POST https://api.forgebench.ai/v1/voice/assets/$ASSET_ID/revoke \
-H "Authorization: Bearer $FORGEBENCH_API_KEY"
curl -X POST https://api.forgebench.ai/v1/voice/consents/$CONSENT_ID/revoke \
-H "Authorization: Bearer $FORGEBENCH_API_KEY"Guardrails on voice
Your guardrails apply to voice as they do to chat:
- Speech input, always. The text sent to Speech Synthesis is scanned before
anything is synthesized. A blocked input returns
400and is audited as blocked, with no call to the provider. - Transcripts, opt-in. Scanning the text produced by Transcription is off
by default. Turn on
transcript_guardrailsin the voice policy to scan it.
What is recorded
Each voice call writes one audit entry and one metering event. The audit entry
records the model, Usage Unit and quantity, the Disclosure outcome (present,
missing, injected, not_first or off), the Voice Asset, and the model's
marking level.
Forgebench never stores recordings or synthesized audio. If you need recordings, keep them in your own systems.
Availability
| Capability | Free | Pro | Enterprise |
|---|---|---|---|
| Voice routes (Transcription, Speech Synthesis, streaming) | — | ||
| Disclosure, marking, Voice Assets, Consent Records | — |
On other plans a voice call returns 402 plan_required, after auth checks
(401, 403). See Billing.
Refusal codes
A refusal body is {"detail": {"code": "...", "message": "..."}}. Speech checks
run in order (Voice Asset, marking, Disclosure), and the first failure sets the
code. Refused calls are audited as blocked and aren't charged.
| Status | Code | Cause |
|---|---|---|
422 | disclosure_missing | Disclosure mode is enforce and the first Speech Synthesis lacks it. |
403 | voice_not_allowed | The voice is not an allowed Voice Asset for this agent. |
403 | marking_required | The model's marking level is below the policy minimum. |
403 | model_not_allowed | The key's model allowlist excludes the model. |
422 | consent_required | Registering a cloned or brand Voice Asset without a valid Consent Record. |
409 | voice_asset_exists | That provider voice is already registered. |
413 | audio too long | Transcription audio is longer than the limit (600 seconds by default). |
402 | plan_required | The plan does not include voice. |
402 | budget codes | A budget or pool limit was reached. See Errors and refusals. |

