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.

Until the email is confirmed, every call answers 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:

  1. Transcribe: the user’s audio → POST /v1/audio/transcriptions → text;
  2. Think: text → POST /v1/chat/completions with stream: true → a reply, sentence by sentence;
  3. Speak: each sentence → POST /v1/audio/speech → mp3, played as it arrives.
The “instant response” trick: play a pre-recorded opener (“One sec…”, “Let me check…”) the moment the user stops speaking — while the pipeline prepares the first sentence, the pause goes unnoticed.

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.

Never embed a key in client-side code or repositories. Revoke a leaked key in the console — the next request made with it is already refused.

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.

ParameterTypeDescription
messagesrequiredarrayThe 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.
modeloptionalstringAccepted and ignored. The answer always comes from fg-voice-1.
streamoptionalbooleanSSE stream. Deltas arrive sentence by sentence — send each straight to speech synthesis.
max_tokensoptionalinteger1–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]
A stream that fails after the headers are sent cannot change its status code: the error arrives inside the stream as 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.

ParameterTypeDescription
inputrequiredstringText up to 4096 characters. Longer is refused with input_too_long.
voiceoptionalstringA voice name or a voice id from the catalog — see Voices. Left out, the default voice is used.
languageoptionalstringen 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.
speedoptionalnumberHow 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.
modeloptionalstringAccepted 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?"}
ParameterTypeDescription
filerequiredfileAudio file up to 32 MB. Missing, the call answers missing_file; unreadable as audio, 400 from the recognizer.
languageoptionalstringOptional hint (en, ru, …). Left out, the language is detected from the audio; given, it makes recognition faster and sharper.
promptoptionalstringContext 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.
modeloptionalstringAccepted 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.

The older names 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()));
}
OpenAIFG
gpt-4o, gpt-4o-minifg-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-1fg-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"
  }
}
StatuscodeWhat to do
400missing_input, input_too_long, unknown_voice, missing_file, missing_messagesThe message names the field. unknown_voice also lists the names that would have been accepted.
401missing_api_key / invalid_api_keyCheck the Authorization: Bearer fg-… header, and that the key has not been revoked.
403email_unverifiedConfirm the email on the account in the console. The key itself is fine.
404nullEither the path does not exist, or the voice id is not in the catalog. The message says which.
429rate_limit_exceededMore than 120 requests in a minute on one key. Wait out the minute; there is no Retry-After header to read.
429daily_quota_exceededThe account’s daily beta quota is used up. The counter resets at midnight UTC.
500api_errorOur side. Retry with exponential backoff.
503database_unavailable, stt_provider_unavailableA dependency is briefly down and the call was not charged. Retry shortly.

Limits & quotas

LimitValueApplies to
Requests a minute120Per key. Over it: 429 rate_limit_exceeded.
Requests a day2,000Per account, all keys together, reset at midnight UTC. Over it: 429 daily_quota_exceeded.
Active keys10Per account. Revoked keys do not count.
Text to speak4,096 charactersPer /audio/speech call.
Audio to recognise32 MBPer file, per /audio/transcriptions call.
Conversation kept40 messagesPer /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.

Free in beta — 2,000 requests a day, no card. Paid plans will be announced in advance. Existing keys will keep working.
Copied