Overview
The Voices API provides endpoints for retrieving and managing voice clones. Voices come in two variants:
- SYSTEM - Pre-trained voices available to all users
- CUSTOM - User-uploaded voices scoped to an organization
Get All Voices
Retrieve all available voices with optional search filtering.
trpc.voices.getAll.useQuery({ query: "narrator" });
Case-insensitive search term to filter voices by name or description
Response
Array of custom voices owned by the organization
Display name of the voice
Optional description of the voice characteristics
Voice category (e.g., “narrator”, “character”, “commercial”)
Primary language of the voice
variant
'CUSTOM' | 'SYSTEM'
required
Voice type (always “CUSTOM” in this array)
Array of system voices available to all usersShow Same Voice object structure as custom
Contains the same fields as custom voices, with variant always set to “SYSTEM”
Implementation
export const voicesRouter = createTRPCRouter({
getAll: orgProcedure
.input(
z
.object({
query: z.string().trim().optional(),
})
.optional(),
)
.query(async ({ ctx, input }) => {
const searchFilter = input?.query
? {
OR: [
{
name: {
contains: input.query, mode: "insensitive" as const
}
},
{
description: {
contains: input.query,
mode: "insensitive" as const,
},
},
],
}
: {};
const [custom, system] = await Promise.all([
prisma.voice.findMany({
where: {
variant: "CUSTOM",
orgId: ctx.orgId,
...searchFilter,
},
orderBy: { createdAt: "desc" },
}),
prisma.voice.findMany({
where: {
variant: "SYSTEM",
...searchFilter,
},
orderBy: { name: "asc" },
}),
]);
return { custom, system };
}),
});
Delete Voice
Delete a custom voice owned by the organization.
trpc.voices.delete.useMutation();
Only CUSTOM voices can be deleted. System voices are protected and will return a NOT_FOUND error if deletion is attempted.
ID of the custom voice to delete
Response
Returns true when deletion is successful
Implementation
delete: orgProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
const voice = await prisma.voice.findUnique({
where: {
id: input.id,
variant: "CUSTOM",
orgId: ctx.orgId,
},
select: { id: true, r2ObjectKey: true },
});
if (!voice) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Voice not found",
});
}
await prisma.voice.delete({ where: { id: voice.id } });
if (voice.r2ObjectKey) {
// Delete audio file from R2 storage
await deleteAudio(voice.r2ObjectKey).catch(() => {});
}
return { success: true };
}),
Error Codes
| Code | Description | When It Occurs |
|---|
UNAUTHORIZED | User not authenticated | Missing or invalid session |
FORBIDDEN | Missing organization context | User not in an organization |
NOT_FOUND | Voice not found | Voice doesn’t exist, is a system voice, or belongs to another org |
Storage Notes
- Custom voice audio files are stored in Cloudflare R2
- Audio files are stored with the key pattern:
voices/custom/{orgId}/{voiceId}
- When a voice is deleted, the database record is removed immediately
- The associated R2 audio file is deleted asynchronously (fire-and-forget)
- Failed R2 deletions are silently caught to avoid blocking the response
In production, consider implementing background jobs, retries, or cron jobs for more robust audio file cleanup.