API Reference
An OpenAI-compatible gateway to the Google Gemini web app. Point any OpenAI-style client at this
server's /v1 base URL and use a proxy API key as the bearer token.
Overview
The proxy exposes the subset of the OpenAI API needed for chat and image generation:
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /v1/chat/completions | Bearer key | Chat completion (streaming + non-streaming, text + images) |
| POST | /v1/responses | Bearer key | OpenAI Responses API (used by n8n's OpenAI node, newer SDKs) |
| POST | /v1/images/generations | Bearer key | Generate image(s) from a prompt (Gemini native) |
| POST | /v1/images/edits | Bearer key | Edit an image with a prompt (multipart; img-to-img) |
| POST | /v1/audio/transcriptions | Bearer key | Transcribe an audio file (emulated via Gemini) |
| POST | /v1/audio/translations | Bearer key | Transcribe + translate audio to English (emulated) |
| POST | /v1/moderations | Bearer key | Classify text for policy violations (emulated) |
| POST | /v1/embeddings | Bearer key | Text embeddings via the official Google AI API (needs a Google AI key) |
| POST | /v1/messages | x-api-key or Bearer | Anthropic Messages API — Claude model IDs mapped to Gemini |
| GET | /v1/models | Bearer key | List models available to the key's account |
| GET | /v1/models/{id} | Bearer key | Retrieve a single model |
| GET | /v1/images/proxy | Signed URL | Serves generated images (used internally by responses) |
| GET | /healthz | none | Health check → ok |
Authentication
Send your proxy API key (created in the Dashboard) as a bearer token:
Authorization: Bearer sk-gem-xxxxxxxxxxxxxxxxxxxxxxxx
Keys are minted in the Dashboard and map to a stored Gemini account. The Google account cookies
(__Secure-1PSID / __Secure-1PSIDTS) live server-side; clients never see them.
Base URL
https://spark.payfara.com/v1
POST /v1/chat/completions
Creates a model response for the given conversation.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
model | string | no | Model slug, display name, internal id, or alias. See /v1/models. Defaults to the account's default model. |
messages | array | yes | List of {role, content}. Roles: system, user, assistant, tool. Multi-turn is flattened into one prompt. |
stream | boolean | no | If true, responds with server-sent events. Default false. |
temperature, max_tokens, user | — | no | Accepted for compatibility; the web app does not honor sampling params. |
Example request
curl https://spark.payfara.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' \
-d '{
"model": "gemini-3-flash",
"messages": [{"role": "user", "content": "Ping"}]
}'
Example response (non-streaming)
{
"id": "chatcmpl-f06c3dd488864a46ba1b0052",
"object": "chat.completion",
"created": 1779271704,
"model": "gemini-3-flash",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Pong" },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2 }
}
Token counts are character-based estimates (the web app doesn't report tokens).
Streaming
With "stream": true the response is text/event-stream using OpenAI's chunk format. Each event is a chat.completion.chunk; the stream terminates with data: [DONE].
curl -N https://spark.payfara.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"model":"gemini-3-flash","stream":true,
"messages":[{"role":"user","content":"Count 1 to 5"}]}'
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"1,"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":" 2, 3, 4, 5"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
POST /v1/responses
OpenAI's newer Responses API. Supported because some clients (notably the
n8n OpenAI node and recent SDKs) use it instead of /chat/completions.
It maps onto the same Gemini pipeline.
Request body
| Field | Type | Description |
|---|---|---|
model | string | Same resolution as chat completions (slug / display name / internal id / alias). |
input | string · array | A plain string, or an array of message items {role, content} where content is a string or content parts (input_text, etc.). |
instructions | string | Optional system prompt, prepended to the conversation. |
stream | boolean | If true, emits the standard Responses SSE event stream (response.output_text.delta, …). |
Example
curl https://spark.payfara.com/v1/responses \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"model":"gemini-3-flash","input":"Ping"}'
{
"id": "resp_…",
"object": "response",
"status": "completed",
"model": "gemini-3-flash",
"output": [
{
"type": "message",
"id": "msg_…",
"status": "completed",
"role": "assistant",
"content": [{ "type": "output_text", "text": "Pong", "annotations": [] }]
}
],
"output_text": "Pong",
"usage": { "input_tokens": 1, "output_tokens": 1, "total_tokens": 2 }
}
Vision & file input
Send images (and documents like PDFs) for the model to read. Use a content-part array in a message; each attachment is uploaded to Gemini and analysed alongside your text. Both a base64 data URI and an http(s) URL are accepted. Limits: up to 10 files, 25 MB each.
Chat Completions (data URI or URL)
curl https://spark.payfara.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{
"model": "gemini-3-flash",
"messages": [{
"role": "user",
"content": [
{ "type": "text", "text": "What is in this image?" },
{ "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBORw0KGgo..." } }
]
}]
}'
A public URL works too: "image_url": { "url": "https://example.com/photo.jpg" }.
Responses API
{
"model": "gemini-3-flash",
"input": [{
"role": "user",
"content": [
{ "type": "input_text", "text": "Summarise this document." },
{ "type": "input_file", "filename": "report.pdf", "file_data": "data:application/pdf;base64,JVBER..." }
]
}]
}
Supported part types: image_url, input_image (image), and
file / input_file (documents). The text prompt and attachments are sent together.
Image generation
Ask the model to generate an image in the prompt. Generated images are appended to the assistant message content as markdown links pointing at a signed image proxy URL (so they render in any markdown client without exposing cookies).
curl https://spark.payfara.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"model":"gemini-3-flash",
"messages":[{"role":"user","content":"Generate an image of a red bicycle on a beach at sunset"}]}'
{
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": ""
},
"finish_reason": "stop"
}],
…
}
The proxy URL returns the raw image bytes (e.g. image/png). Availability of image generation depends on your Gemini account/region.
GET /v1/models
Lists the models available to the key's account, discovered live from Gemini (cached ~hourly).
curl https://spark.payfara.com/v1/models -H "Authorization: Bearer $KEY"
{
"object": "list",
"data": [
{
"id": "gemini-3-flash",
"object": "model",
"created": 1779272775,
"owned_by": "google-gemini-web",
"display_name": "3 Flash",
"internal_id": "fbb127bbb056c959",
"description": "All-around help"
}
]
}
In the model field you can pass any of: the friendly id (gemini-3-flash),
the display_name (3 Flash), the internal_id (fbb127bbb056c959),
or a static alias (gemini-3-pro, gpt-4o, gpt-3.5-turbo).
GET /v1/images/proxy
Internal endpoint that streams a generated/web image fetched with the account's cookies. URLs are produced (and HMAC-signed) by the proxy inside chat responses — you don't construct these yourself.
| Query | Description |
|---|---|
u | base64url-encoded upstream image URL |
a | account id |
s | HMAC signature over u.a |
Returns the image bytes with the upstream Content-Type, or 403 if the signature is invalid.
Dashboard API
Session-authenticated (login cookie), used by the dashboard & playground. Not part of the public OpenAI surface.
| Method | Path | Description |
|---|---|---|
| GET | /api/me | Current user |
| GET / POST | /api/accounts | List / add a Gemini account (cookies) |
| POST | /api/accounts/:id/test | Re-verify an account |
| DELETE | /api/accounts/:id | Delete an account |
| GET / POST | /api/keys | List / create proxy API keys |
| DELETE | /api/keys/:id | Revoke a key |
| GET | /api/usage | Usage summary |
| GET | /api/models | Models for the playground |
| POST | /api/playground/chat | Playground generation (custom SSE) |
| — | /auth/login, /auth/callback, /auth/logout | Google OAuth flow |
Errors
Errors follow the OpenAI error envelope:
{ "error": { "message": "Invalid API key.", "type": "authentication_error" } }
| Status | Meaning |
|---|---|
401 | Missing/invalid bearer key |
400 | Bad request body, or no Gemini account configured for the key |
429 | Gemini usage limit exceeded (code 1037) |
502 | Gemini auth/anti-abuse failure, or upstream error |
Using OpenAI SDKs
Point the official OpenAI SDK at the base URL:
from openai import OpenAI
client = OpenAI(
base_url="https://spark.payfara.com/v1",
api_key="sk-gem-xxxxxxxxxxxxxxxxxxxxxxxx",
)
resp = client.chat.completions.create(
model="gemini-3-flash",
messages=[{"role": "user", "content": "Hello!"}],
)
print(resp.choices[0].message.content)
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://spark.payfara.com/v1",
apiKey: "sk-gem-xxxxxxxxxxxxxxxxxxxxxxxx",
});
const r = await client.chat.completions.create({
model: "gemini-3-flash",
messages: [{ role: "user", content: "Hello!" }],
});
console.log(r.choices[0].message.content);
Images API
POST /v1/images/generations
Generate image(s) from a text prompt using Gemini's native image generation.
curl https://spark.payfara.com/v1/images/generations \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"prompt":"a single red apple on a white background","response_format":"url"}'
{ "created": 1700000000, "data": [ { "url": "https://spark.payfara.com/v1/images/proxy?…" } ] }
response_format: "url" (signed proxy URL, default) or "b64_json" (base64 bytes).
Returned images are full resolution (the proxy fetches the original via Gemini's full-size lookup, not the watermarked preview). To request a smaller render, append &sz= to the proxy URL — e.g. &sz=w512 or &sz=none for the preview.
POST /v1/images/edits
Edit an image with a prompt (image-to-image). multipart/form-data: image (file), prompt, optional response_format.
curl https://spark.payfara.com/v1/images/edits \ -H "Authorization: Bearer $KEY" \ -F "image=@photo.png" \ -F "prompt=add a thick blue border"
Returns the same {created, data:[…]} shape.
Note on reliability: the Gemini web backend decides per-request whether to invoke its image
tool — it sometimes replies with text instead of an image. When that happens the proxy returns a
502 "did not return an image" after a wait. Image generation is therefore best-effort
and intermittent; retry if you get a 502. Availability also depends on your Gemini account/region.
Audio API
Transcription and translation are emulated: the audio file is uploaded to Gemini
and the model is asked to transcribe (or transcribe + translate to English). multipart/form-data
with a file field. Text-to-speech (/v1/audio/speech) is not supported
(returns 501) — the Gemini web backend has no OpenAI-style TTS.
curl https://spark.payfara.com/v1/audio/transcriptions \
-H "Authorization: Bearer $KEY" \
-F "file=@recording.mp3" -F "model=whisper-1"
# -> { "text": "…transcript…" }
curl https://spark.payfara.com/v1/audio/translations \
-H "Authorization: Bearer $KEY" \
-F "file=@recording.mp3"
# -> { "text": "…English translation…" }
Add -F "response_format=text" to get a plain-text body instead of JSON. Accuracy is best-effort (it relies on Gemini's audio understanding, not a dedicated ASR model).
POST /v1/moderations
Classify text for policy violations. Emulated by asking the model to score OpenAI's moderation categories.
curl https://spark.payfara.com/v1/moderations \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"input":"...text to classify..."}'
{
"id": "modr-…",
"model": "gemini-moderation",
"results": [{
"flagged": true,
"categories": { "violence": true, "harassment/threatening": true, "hate": false, … },
"category_scores": { "violence": 0.99, "harassment/threatening": 0.95, … }
}]
}
input may be a string or an array of strings. Scores are model-estimated, not OpenAI's classifier.
POST /v1/embeddings
Text embeddings for vector stores / RAG. The Gemini web backend can't embed, so this
proxies to the official Google AI embeddings API. You must configure a free
Google AI Studio key —
either per Gemini account (dashboard → 🔑 on the account) or globally via the
GOOGLE_AI_API_KEY secret. Clients still authenticate with their sk-gem proxy key.
Request
| Field | Type | Description |
|---|---|---|
input | string · string[] | Text to embed (single or batch). |
model | string | gemini-embedding-001 (default), gemini-embedding-2, text-embedding-004; OpenAI names like text-embedding-3-small map to a default. |
dimensions | number | Optional output dimensionality (e.g. 768) for models that support truncation. |
encoding_format | string | float (default) or base64. |
curl https://spark.payfara.com/v1/embeddings \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"model":"gemini-embedding-001","input":"The quick brown fox"}'
{
"object": "list",
"data": [ { "object": "embedding", "index": 0, "embedding": [0.013, -0.027, ...] } ],
"model": "gemini-embedding-001",
"usage": { "prompt_tokens": 5, "total_tokens": 5 }
}
⚠️ Match the embedding dimensionality across your vector store (the n8n node notes the default is 768-dim). Pass dimensions to control it where supported.
Anthropic Claude API compatibility
The proxy speaks the Anthropic Messages API on POST /v1/messages.
Claude model IDs are mapped to the equivalent Gemini backend automatically — no backend changes needed.
Model mapping
Two-tier resolution happens inside src/routes/anthropic.ts → mapClaudeToGemini():
Tier 1 — Dynamic claude-gemini-* prefix (preferred)
Call GET /v1/models with an Anthropic header first.
The proxy generates a claude-{gemini-slug} ID for every model on your account.
Use that ID in POST /v1/messages — the proxy strips the claude- prefix
to get the exact Gemini slug.
| ID returned by GET /v1/models | What you send in model field | Routed to |
|---|---|---|
claude-gemini-3-flash | claude-gemini-3-flash | gemini-3-flash |
claude-gemini-3-pro | claude-gemini-3-pro | gemini-3-pro |
claude-gemini-3.1-flash-lite | claude-gemini-3.1-flash-lite | gemini-3.1-flash-lite |
claude-gemini-pro | claude-gemini-pro | gemini-pro |
Tier 2 — Keyword fallback (for hardcoded Claude names)
If the model name does not start with claude-gemini-, it is matched
by keyword. This handles any client that hardcodes official Claude model IDs
(e.g. Claude Code, the Anthropic SDKs, n8n Anthropic node).
| Model name sent | Keyword matched | Routed to |
|---|---|---|
claude-3-5-sonnet-20241022 | contains sonnet | gemini-3-flash |
claude-sonnet-4-6 | contains sonnet | gemini-3-flash |
claude-opus-4-7 | contains opus | gemini-3-pro |
claude-3-opus-20240229 | contains opus | gemini-3-pro |
claude-haiku-4-5 | contains haiku | gemini-3-flash |
claude-3-5-haiku-20241022 | contains haiku | gemini-3-flash |
| anything else | default | gemini-3-flash |
Tier 2 covers all current Anthropic model families: Opus → Pro (most capable),
Sonnet / Haiku → Flash (fast). Future Claude releases will fall to
gemini-3-flash by default until the keyword list is extended in
src/routes/anthropic.ts.
Authentication
Use your proxy key (sk-gem-…) as either Authorization: Bearer <key>
or x-api-key: <key>. Both are accepted.
POST /v1/messages — non-streaming
curl https://spark.payfara.com/v1/messages \
-H "x-api-key: $KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello, Claude!"}]
}'
Response follows the Anthropic Messages format:
{
"id": "msg_01XFDUDYJgAACTU3VRZBmF",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "Hello! How can I help you?"}],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {"input_tokens": 10, "output_tokens": 9}
}
POST /v1/messages — streaming
curl -N https://spark.payfara.com/v1/messages \
-H "x-api-key: $KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"stream": true,
"messages": [{"role": "user", "content": "Write a haiku"}]
}'
Streaming emits the full Anthropic SSE event sequence:
event: message_start
data: {"type":"message_start","message":{...}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: ping
data: {"type":"ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Over"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":12}}
event: message_stop
data: {"type":"message_stop"}
GET /v1/models (Anthropic format)
If the request includes an anthropic-version or x-api-key header,
GET /v1/models returns models in the Anthropic list format:
curl https://spark.payfara.com/v1/models \ -H "x-api-key: $KEY" \ -H "anthropic-version: 2023-06-01"
{
"data": [
{"type":"model","id":"claude-gemini-3-flash","display_name":"3 Flash — All-around help (Gemini via proxy)","created_at":"..."},
{"type":"model","id":"claude-gemini-3-pro","display_name":"Pro — Advanced math & code (Gemini via proxy)","created_at":"..."},
{"type":"model","id":"claude-gemini-3.1-flash-lite","display_name":"3.1 Flash-Lite — Fastest answers (Gemini via proxy)","created_at":"..."},
{"type":"model","id":"claude-opus-4-7","display_name":"Claude Opus 4.7 → Gemini 3 Pro","created_at":"2025-03-13T00:00:00Z"},
{"type":"model","id":"claude-sonnet-4-6","display_name":"Claude Sonnet 4.6 → Gemini 3 Flash","created_at":"2025-03-13T00:00:00Z"},
...
],
"has_more": false
}
The first group (claude-gemini-*) is dynamically generated from your account's live model list.
The second group is the static Claude alias fallbacks always appended at the end.
System prompt & multi-turn
The top-level system field (string or content blocks) is prepended as a system instruction.
Multi-turn messages are flattened into a single labelled transcript, the same way the OpenAI endpoint works.
Vision input
Attach images via Anthropic-style content blocks:
{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": "<base64>"}},
{"type": "text", "text": "What is in this image?"}
]
}
URL-sourced images ("type":"url") are also supported. Attachments are uploaded to Gemini before inference.
SDK example (Anthropic Python SDK)
import anthropic
client = anthropic.Anthropic(
api_key="sk-gem-your-proxy-key",
base_url="https://spark.payfara.com",
)
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[{"role": "user", "content": "Explain quantum entanglement simply."}],
)
print(message.content[0].text)
SDK example (Anthropic JS/TS SDK)
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: "sk-gem-your-proxy-key",
baseURL: "https://spark.payfara.com",
});
const msg = await client.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1024,
messages: [{ role: "user", content: "What is 2+2?" }],
});
console.log(msg.content[0].text);
n8n integration
To use the proxy from n8n's OpenAI node ("Message a Model"):
- Create an OpenAI credential. Set API Key to your
sk-gem-…proxy key. - Set Base URL to
https://spark.payfara.com/v1. - In the node, pick a model from the list (it loads via
/v1/models) and send your message.
n8n's OpenAI node uses the Responses API (/v1/responses) under the hood,
which this proxy implements. The router also tolerates a doubled /v1 prefix, so the integration
works whether or not your Base URL already includes /v1.
n8n operation support
Status of the n8n OpenAI node's 16 operations against this proxy:
| Resource | Operation | Status |
|---|---|---|
| Text | Message a Model | ✅ supported (native) |
| Text | Classify Text for Violations | ✅ supported (emulated) |
| Image | Analyze Image | ✅ supported (vision) |
| Image | Generate an Image | ⚠️ supported (native, intermittent — retry on 502) |
| Image | Edit an Image | ⚠️ supported (native, intermittent) |
| Audio | Transcribe a Recording | ✅ supported (emulated) |
| Audio | Translate a Recording | ✅ supported (emulated) |
| Audio | Generate Audio (TTS) | ❌ not supported (no Gemini TTS) → 501 |
| Assistant | Create / Update / Delete / List | ❌ not implemented (Assistants API) |
| Assistant | Message an Assistant | ❌ not implemented (Threads/Runs) |
| File | Upload / List / Delete a File | ❌ not implemented (Files API) |
13 of 16 operations work. "Emulated" means it's driven by prompting the model rather than a dedicated endpoint (best-effort accuracy). Assistants & Files APIs are stateful OpenAI features not yet implemented.