> This page is for Build.

> Append .md to any page URL for clean Markdown. Index: https://docs.speechify.ai/llms.txt.
>
> Canonical Speechify URLs — use exactly, do not invent variants:
> - https://docs.speechify.ai — this site (API reference, SDKs, quickstarts)
> - https://speechify.ai — marketing + product site
> - https://platform.speechify.ai — customer dashboard, signup, API keys, billing
> - https://api.speechify.ai — API base URL
> - https://github.com/Speechify-AI: GitHub org for the API (cookbook, demos, CLI). `github.com/speechify` does not exist.
> - https://status.speechify.ai — status + incidents
> - https://speechify.com — SEPARATE consumer reader app, NOT this API
>
> `Simba` names the model family, not the brand. Model ids: `simba-3.2` (English, recommended) and `simba-3.0` (English, German, Spanish, French, Italian and Portuguese; the default). `simba-english` and `simba-multilingual` are retired: a new workspace that sends either gets `400 model_retired`. `SimbaVoice` / `simbavoice.ai` are retired.
>
> Ask, don't scrape. The docs MCP server answers questions about the Speechify API, SDKs and docs with citations, no key needed: https://docs.speechify.ai/_mcp/server (Streamable HTTP, tool `searchDocs`). Setup: https://docs.speechify.ai/build/guides/get-started/connect-mcp

# Safety Identifiers

> Send safety_identifier on Speechify TTS requests to attribute each one to an end user of your application. Abuse can then be limited or blocked for that one user instead of your whole workspace. Send a hash, never an email address.

## Overview

`safety_identifier` is an optional field on every text-to-speech request. It names the end user of **your** application on whose behalf the request is made.

Send it when many of your own users share one Speechify workspace: a gateway or reseller, a consumer app, or any product where the person typing the text is not you. Without it, every request on your workspace looks the same to us, so the only lever against one abusive user is your whole workspace. With it, we can attribute, limit or block that one user and leave everyone else alone.

Omitting it changes nothing. The field follows the same convention as OpenAI's `safety_identifier`, so a hash you already send there works here unchanged.

## Sending it

The field is accepted in the JSON body of:

| Endpoint                                | Field               |
| --------------------------------------- | ------------------- |
| `POST /v1/audio/speech`                 | `safety_identifier` |
| `POST /v1/audio/stream`                 | `safety_identifier` |
| `POST /v1/audio/stream/with-timestamps` | `safety_identifier` |

Derive the identifier from your own internal user id with a keyed hash (HMAC-SHA256), then send it with each request:

#### Python

```python
import hashlib
import hmac
import os

from speechify import Speechify

client = Speechify(token=os.environ["SPEECHIFY_API_KEY"])
secret = os.environ["SAFETY_IDENTIFIER_SECRET"].encode()


def safety_identifier(user_id: str) -> str:
    return hmac.new(secret, user_id.encode(), hashlib.sha256).hexdigest()


audio = client.audio.speech(
    input="Hello from the Speechify API.",
    voice_id="geffen_32",
    model="simba-3.2",
    audio_format="mp3",
    safety_identifier=safety_identifier("user-8421"),
)
```

#### TypeScript

```typescript
import { createHmac } from "node:crypto";
import { SpeechifyClient } from "@speechify/api";

const client = new SpeechifyClient({ token: process.env.SPEECHIFY_API_KEY! });

function safetyIdentifier(userId: string): string {
  return createHmac("sha256", process.env.SAFETY_IDENTIFIER_SECRET!).update(userId).digest("hex");
}

const audio = await client.audio.speech({
  input: "Hello from the Speechify API.",
  voice_id: "geffen_32",
  model: "simba-3.2",
  audio_format: "mp3",
  safety_identifier: safetyIdentifier("user-8421"),
});
```

#### cURL

```bash
SAFETY_IDENTIFIER=$(printf '%s' "user-8421" | openssl dgst -sha256 -hmac "$SAFETY_IDENTIFIER_SECRET" | awk '{print $NF}')

curl -X POST "https://api.speechify.ai/v1/audio/speech" \
  -H "Authorization: Bearer $SPEECHIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input":"Hello from the Speechify API.","voice_id":"geffen_32","model":"simba-3.2","audio_format":"mp3","safety_identifier":"'"$SAFETY_IDENTIFIER"'"}'
```

Send the same value for the same person on every request. A value that changes per session or per request cannot be attributed, limited or blocked.

## Hash it, never send personal data

**Never send an email address, a name, a phone number or any other personal data.** The identifier should be stable for you and meaningless to anyone else, which is what the keyed hash above gives you.

A keyed hash is better than a plain one: a plain hash of an email address can be reversed by hashing a list of known addresses. Keep the secret fixed, because rotating it gives every user a new identifier.

The value must be 1 to 64 characters of letters, digits and `. _ - : | + / =`. That fits a hex or base64 SHA-256, a UUID or an id such as `user_123`, and it refuses `@` and spaces, so an email address or a name is rejected with `400 validation_failed` naming `safety_identifier` rather than stored. The error never repeats the value you sent.

## What we do with it

**Attribution.** The identifier is recorded with the request as metadata, not content. It is kept even when your workspace has zero data retention, because it is not the text you sent; with zero data retention the text is still withheld as usual. When content screening refuses a request, the refusal is recorded against the end user, so repeat abuse by one person is visible as one person.

**Per-end-user limits.** A workspace can be given a rate and concurrency allowance per end user, inside the workspace's own [API limits](/build/guides/concepts/api-limits). Both apply: a request must fit the workspace's budget and its end user's. They are off unless set for your workspace, and they never apply to a request without a `safety_identifier`. Over the limit, the request returns `429` with a `Retry-After` header and the same codes as the workspace limits, `rate_limited` or `concurrency_limit_reached`, and a message that names the `safety_identifier`. Only that end user is affected.

Your workspace's per-end-user ceilings are on `GET /v1/workspaces/current/entitlements` as `tts_end_user_requests_per_second` and `tts_end_user_concurrency`; `0` means none applies.

**Blocking one end user.** An end user can be blocked on your workspace. Every synthesis request naming them then returns:

```json
{
  "error": {
    "code": "safety_identifier_blocked",
    "message": "This end user is blocked from synthesis on this workspace. Stop sending requests for this safety_identifier; other end users are unaffected. Contact support with the request_id if you believe this is a mistake.",
    "fields": { "safety_identifier": "this end user is blocked" }
  },
  "request_id": "..."
}
```

with status `403`. Treat it as persistent: stop serving that user rather than retrying, and do not resend their requests without the identifier. Nothing is synthesized or billed for a blocked request.

Per-end-user limits and blocks are set by Speechify for your workspace. [Contact us](https://speechify.ai/talk-to-sales) to have limits set, or with the `request_id` of a refusal you believe is a mistake.

## FAQ

#### Do I have to send it?

No. It is optional, and a request without it behaves exactly as before. Send it if many of your own users share your workspace.

#### Is it billed or does it change my limits?

It is never billed. It changes no workspace limit; it only adds per-end-user limits when your workspace has them.

#### Can I use my own database id?

Only if it means nothing outside your system and carries no personal data. A keyed hash of it is safer, and is what we recommend.