Skip to main content

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" });

Input Schema

query
string
Case-insensitive search term to filter voices by name or description

Response

custom
Voice[]
Array of custom voices owned by the organization
system
Voice[]
Array of system voices available to all users

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.

Input Schema

id
string
required
ID of the custom voice to delete

Response

success
boolean
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

CodeDescriptionWhen It Occurs
UNAUTHORIZEDUser not authenticatedMissing or invalid session
FORBIDDENMissing organization contextUser not in an organization
NOT_FOUNDVoice not foundVoice 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.

Build docs developers (and LLMs) love