FG API Documentation
Real-time voice AI for business: speech recognition, dialogue and synthesis in three HTTP calls. Base URL: https://fg-platform.com/v1 (beta).
1. Get an API key
Sign up, confirm the email on the account, then create a key in the console. A key like fg-… is shown only once — save it. An account holds up to ten active keys at a time; revoke one to make room for another.
403 email_unverified — including calls made with a key that was created successfully. Confirming the address is the whole fix; the key you already have starts working.2. Your first audio — in one command
curl https://fg-platform.com/v1/audio/speech \ -H "Authorization: Bearer $FG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "fg-voice-1", "input": "Sir, the platform is connected." }' --output hello.mp3 && open hello.mp3
There is no voice field in that command on purpose: left out, the call uses the platform’s default voice. Choosing another one is in Voices.
3. The full voice loop
The assistant is a pipeline of three calls. Each step is its own endpoint, with your code in between:
- Transcribe: the user’s audio →
POST /v1/audio/transcriptions→ text; - Think: text →
POST /v1/chat/completionswithstream: true→ a reply, sentence by sentence; - Speak: each sentence →
POST /v1/audio/speech→ mp3, played as it arrives.
Authentication
Every request carries an Authorization: Bearer fg-… header — the same header on all endpoints, including the multipart one. Keys are created and revoked in the console; we store only a hash of the key, never the key itself, which is why a lost key cannot be shown again.
The API answers cross-origin browser requests: Access-Control-Allow-Origin: *, and a preflight OPTIONS returns 204. That is for prototypes — a key that reaches the browser reaches the visitor, so keep it on your server in production.
Model
GET/v1/models
fg-voice-1 — one model behind all three calls: recognition, dialogue, synthesis. This is also the cheapest way to check that a key works.
curl https://fg-platform.com/v1/models \
-H "Authorization: Bearer $FG_API_KEY"{
"object": "list",
"data": [{
"id": "fg-voice-1",
"object": "model",
"created": 1782000000,
"owned_by": "fg-ai"
}]
}The model field inside a request body is accepted for SDK compatibility and then ignored — there is one model, and every response names it fg-voice-1 whatever you sent.
Chat completions
POST/v1/chat/completions
Chat with the model. The format matches OpenAI: the same messages array, the same chat.completion response, the same chunked streaming. By default the model is tuned for speech: short phrases, no markdown.
| Parameter | Type | Description |
|---|---|---|
| messagesrequired | array | The conversation: {role: "system"|"user"|"assistant", content: "…"}. Your own system message sets the persona in place of the default one; the platform’s own rules are always applied on top of it. |
| modeloptional | string | Accepted and ignored. The answer always comes from fg-voice-1. |
| streamoptional | boolean | SSE stream. Deltas arrive sentence by sentence — send each straight to speech synthesis. |
| max_tokensoptional | integer | 1–4096, default 1024. A value outside that range is clamped to it rather than refused. |
Long conversations are trimmed before the model sees them, newest first: the last 400 messages, at most 8,000 characters from each and 100,000 characters in total. Nothing is refused for being too long — the older end is simply dropped, so keep anything that must survive in your system message. In practice a spoken call is bounded by the character budget long before the message count, so a whole conversation reaches the model as one context — including the turn in which the assistant introduced itself.
If your messages array opens with an assistant message — the line your client already played to the caller — it is moved into the system prompt rather than dropped, and the model is told it has already been spoken. That is how a client that greets on connect avoids being greeted twice.
Without your own system message you get the platform’s voice assistant as it behaves on our own site: it introduces itself once per conversation and not again, it does not open a reply with a hello once the conversation is under way, and it does not repeat a sentence it has already said. With your own system message your persona is in charge — only the identity rule and the “say it once” rule are added on top, and the text of the reply is returned exactly as the model wrote it.
curl https://fg-platform.com/v1/chat/completions \ -H "Authorization: Bearer $FG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "fg-voice-1", "messages": [ { "role": "system", "content": "You answer for a car service. Be brief." }, { "role": "user", "content": "Do you fix brakes on a Volvo?" } ] }'
Response
{
"id": "chatcmpl-991140a274a2e38c58736ebc",
"object": "chat.completion",
"created": 1787333566,
"model": "fg-voice-1",
"choices": [{
"index": 0,
"message": { "role": "assistant", "content": "Yes, we do. We service brakes on Volvos…" },
"finish_reason": "stop"
}],
"usage": { "prompt_tokens": 431, "completion_tokens": 57, "total_tokens": 488 }
}Response (stream: true)
The first chunk opens the message and carries no text; every chunk after it is a finished sentence, ready to hand to synthesis; the last one carries finish_reason: "stop" and is followed by [DONE].
# abridged: id, created and model repeat on every chunk data: {"object":"chat.completion.chunk","model":"fg-voice-1","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]} data: {"object":"chat.completion.chunk","model":"fg-voice-1","choices":[{"index":0,"delta":{"content":"FG is an AI company focused on building advanced voice models and conversational AI systems. "},"finish_reason":null}]} data: {"object":"chat.completion.chunk","model":"fg-voice-1","choices":[{"index":0,"delta":{"content":"We're developing technology that lets people interact with AI through natural speech."},"finish_reason":null}]} data: {"object":"chat.completion.chunk","model":"fg-voice-1","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} data: [DONE]
data: {"error": {…}} and the connection then closes without [DONE]. Treat a stream that ends without [DONE] as a failed call.Speech synthesis
POST/v1/audio/speech
Text → audio/mpeg: mp3, 44.1 kHz, mono, 128 kbps. The body arrives chunked and without a Content-Length, so a player can start on the first bytes instead of waiting for the file. A sentence takes a fraction of a second on our side — the measured figures are on the Voice API page.
| Parameter | Type | Description |
|---|---|---|
| inputrequired | string | Text up to 4096 characters. Longer is refused with input_too_long. |
| voiceoptional | string | A voice name or a voice id from the catalog — see Voices. Left out, the default voice is used. |
| languageoptional | string | en or ru. Left out, it follows the chosen voice, and falls back to the alphabet the text is written in. Any other value is treated as absent. |
| speedoptional | number | How fast to speak, as a share of the ordinary pace: 1 is normal, 0.75 is the pace a price or an account number is read at, 1.15 is brisk. Clamped to 0.6–1.3; anything else is ignored. See the note below on X-FG-Pace-Rate. |
| modeloptional | string | Accepted and ignored. |
curl https://fg-platform.com/v1/audio/speech \ -H "Authorization: Bearer $FG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": "Sir, the platform is connected.", "voice": "sergei" }' --output reply.mp3 # → 200, audio/mpeg — mp3, 44100 Hz, mono, 128 kbps
Numbers, dates, phone numbers and abbreviations are rewritten into words before they are spoken, so “$1,200” and “14.08.2026” come out as a person would read them. What you send is what is spoken; what you get back is audio only — there is no JSON envelope on a successful call.
When speed asks for more than the synthesiser’s own dial can reach, the response carries X-FG-Pace-Rate — the playback rate to set on your player (audio.playbackRate, or a tempo filter) to arrive at the pace you asked for, pitch unchanged. The header is only present when it is not 1, and ignoring it is safe: the audio is still slower or faster than normal, just not by the whole amount.
Speech recognition
POST/v1/audio/transcriptions
Audio → text. multipart/form-data with a file field: webm/opus, mp3, wav, m4a. A built-in filter drops noise and recognition “hallucinations” — silence returns an empty text, and the assistant just keeps listening.
curl https://fg-platform.com/v1/audio/transcriptions \ -H "Authorization: Bearer $FG_API_KEY" \ -F file=@question.mp3 \ -F model=fg-voice-1 # → {"text": "What is the revenue this week?"}
| Parameter | Type | Description |
|---|---|---|
| filerequired | file | Audio file up to 32 MB. Missing, the call answers missing_file; unreadable as audio, 400 from the recognizer. |
| languageoptional | string | Optional hint (en, ru, …). Left out, the language is detected from the audio; given, it makes recognition faster and sharper. |
| promptoptional | string | Context for this fragment — names, product terms, the previous line of the conversation. Up to 600 characters are used, and they are a hint for recognition, not text that gets transcribed. |
| modeloptional | string | Accepted and ignored. |
Voices
Leave voice out and the call uses the platform default: sergei, a male voice used for both English and Russian text. It is the only voice currently reachable by name.
alex, andrey, andrey-en, andrey-ru, anastasia and anastasia-ru are still accepted by the parameter, but the voices behind them are no longer in the catalog and the call fails with 404. Do not build on those names — use the default, or a voice id.By id, any voice from the catalog works — 901 of them at the time of writing. Pass the id in the same voice field:
curl https://fg-platform.com/v1/audio/speech \ -H "Authorization: Bearer $FG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "voice": "f014dce5-df0e-4cfa-98e1-bd4bb73bb0b1", "input": "What is the revenue this week?" }' --output question.mp3
An id that is not in the catalog answers 404; a name that is neither a known alias nor an id is refused up front with 400 unknown_voice, and the message lists the names it would have accepted.
Speech in and out: English and Russian. Text: any language the visitor writes in. Recognition takes the language from the audio — nothing to set per request.
Embed on your site
The other half of the platform needs no API at all. An agent built in the console — it reads your site, answers in writing and out loud, and collects contacts — goes onto a page as a single line, anywhere before </body>:
<script src="https://fg-platform.com/assets/global-voice-widget.js" data-agent="agt-…" async></script>
data-agent is the agent’s id, which the console shows next to the ready-made snippet. Everything else — the greeting, the design, the voice, whether contacts are collected, whether the agent is paused — is set in the console and picked up by the widget on its own: the line on your site is written once and never edited again.
The widget carries no API key, and its conversations are counted against your platform plan rather than against the API beta quota below.
Migrate from OpenAI
The API is compatible with OpenAI’s format. In the official SDK you change two lines — the base URL and the key — and the rest of the file stands:
import OpenAI from 'openai'; import fs from 'node:fs'; // before: new OpenAI({ apiKey: 'sk-…' }) const fg = new OpenAI({ baseURL: 'https://fg-platform.com/v1', apiKey: process.env.FG_API_KEY, // fg-… }); // the rest of your code is unchanged: const heard = await fg.audio.transcriptions.create({ file: fs.createReadStream('question.mp3'), model: 'fg-voice-1', }); const stream = await fg.chat.completions.create({ model: 'fg-voice-1', stream: true, messages: [{ role: 'user', content: heard.text }], }); for await (const part of stream) { const line = part.choices[0].delta.content; // a whole sentence if (!line) continue; const speech = await fg.audio.speech.create({ model: 'fg-voice-1', voice: 'sergei', input: line, }); play(Buffer.from(await speech.arrayBuffer())); }
| OpenAI | FG |
|---|---|
gpt-4o, gpt-4o-mini… | fg-voice-1 — and the field is ignored, so an unchanged model name also works |
voice: "alloy" / "verse" | voice: "sergei", a catalog id, or nothing at all for the default |
whisper-1 | fg-voice-1 (you can leave the model field unchanged — it’s ignored) |
| Realtime API (WebRTC/WS) | Three HTTP calls + an opener line (see Quickstart) — comparable UX without WebRTC |
embeddings, images, assistants, … | Not implemented. An unknown path under /v1 answers 404 with a JSON body, not an HTML page |
Errors
Errors follow the OpenAI shape: an HTTP status, and a JSON body with error.message, error.type, error.param and error.code. When the failure comes from a provider rather than from your request, error.code is null — so read the status, then the code if there is one.
{
"error": {
"message": "Invalid or revoked API key.",
"type": "authentication_error",
"param": null,
"code": "invalid_api_key"
}
}| Status | code | What to do |
|---|---|---|
| 400 | missing_input, input_too_long, unknown_voice, missing_file, missing_messages | The message names the field. unknown_voice also lists the names that would have been accepted. |
| 401 | missing_api_key / invalid_api_key | Check the Authorization: Bearer fg-… header, and that the key has not been revoked. |
| 403 | email_unverified | Confirm the email on the account in the console. The key itself is fine. |
| 404 | null | Either the path does not exist, or the voice id is not in the catalog. The message says which. |
| 429 | rate_limit_exceeded | More than 120 requests in a minute on one key. Wait out the minute; there is no Retry-After header to read. |
| 429 | daily_quota_exceeded | The account’s daily beta quota is used up. The counter resets at midnight UTC. |
| 500 | api_error | Our side. Retry with exponential backoff. |
| 503 | database_unavailable, stt_provider_unavailable | A dependency is briefly down and the call was not charged. Retry shortly. |
Limits & quotas
| Limit | Value | Applies to |
|---|---|---|
| Requests a minute | 120 | Per key. Over it: 429 rate_limit_exceeded. |
| Requests a day | 2,000 | Per account, all keys together, reset at midnight UTC. Over it: 429 daily_quota_exceeded. |
| Active keys | 10 | Per account. Revoked keys do not count. |
| Text to speak | 4,096 characters | Per /audio/speech call. |
| Audio to recognise | 32 MB | Per file, per /audio/transcriptions call. |
| Conversation kept | 40 messages | Per /chat/completions call — plus 8,000 characters per message and 100,000 in total. |
GET /v1/models is not counted against the daily quota, but it is counted by the per-minute limiter.