API Reference
An OpenAI- and Anthropic-compatible gateway to the Google Gemini web app, Google Antigravity,
Alibaba Qwen, ChatGPT (GPT-5) and
duck.ai. Point any OpenAI-style
client at this server's /v1 base URL and use a proxy API key as the bearer token — the
key's linked account selects the backend.
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. |
tools | array | no | Function definitions, in OpenAI's {type:"function", function:{name, description, parameters}} shape. The legacy functions array is accepted as an alias, but replies always use the modern tool_calls shape, never function_call. See Function calling. |
tool_choice | string / object | no | auto (default), none, required, or {"type":"function","function":{"name":"…"}}. |
parallel_tool_calls | boolean | no | Default true. Set false to cap a turn at one call. |
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]
Function calling (tools)
Send tools and the model can answer with tool_calls instead of prose,
exactly as the OpenAI API does — same request fields, same response shape,
finish_reason: "tool_calls", and message.content: null when the turn is
only a call. Reply with a role: "tool" message carrying the matching
tool_call_id to continue the loop. It works on every backend — Gemini,
Antigravity, Qwen, ChatGPT and duck.ai — and on all three surfaces:
/v1/chat/completions, /v1/responses (as function_call output
items) and /v1/messages (as Anthropic tool_use blocks).
How it works, and what that costs you
On the ChatGPT backend it is native. That backend is the OpenAI Responses API underneath, so
your declarations are sent as declarations and the calls come back over the protocol — nothing is
emulated, and tool_choice is enforced by the decoder rather than requested politely.
The caveats below do not apply to it, apart from streaming.
The other four — Gemini, Antigravity, Qwen and duck.ai — are consumer chat surfaces with no
function-calling wire protocol, so there Spark emulates it: your tool schemas are compiled
into a system preamble, and the block the model writes is parsed back into tool_calls.
That is a real difference from the OpenAI API and it is worth knowing about:
- Streaming buffers. A tool call is indistinguishable from prose until it is complete,
so
stream: truewithtoolsgenerates the whole reply first and then replays it as a well-formed stream. You still get valid SSE; you just get it at the end. Requests withouttoolsstream as before. tool_choiceis best-effort (except on ChatGPT).requiredand a named tool are instructions to the model, not a decoder constraint — the emulated backends expose no way to force a token sequence. A strong model complies reliably; a weak one may answer in prose instead. When you name a tool, calls to any other tool are dropped, so you never get one you didn't ask for — but you may get none.- A plain
```jsonblock is content, not a call. Only the<tool_call>form (or a fence explicitly labelledtool_call) is read as a call, so asking the model to explain one of your tools doesn't get that example executed. The exception is when you settool_choicetorequiredor a named tool — then a bare JSON block is accepted, because you asked for a call. - Unknown tool names are dropped. A call naming a tool you did not declare is left in the reply as ordinary text rather than handed to your client, which would throw on it. The same goes for a block whose JSON cannot be parsed — a malformed call degrades to something you can see.
- Schemas cost context. The preamble is re-sent every turn and competes with your conversation for the model's context window. Keep tool lists tight.
- Streaming buffers on ChatGPT too, even though it is native — a turn has to be complete before Spark knows whether it ended in a call. Everything else on that backend behaves like the real thing.
Example
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": "What is the weather in Dhaka?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}}}]
}'
{
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_5f1c9a2b7e0d4c8fa3b61d92",
"type": "function",
"index": 0,
"function": {"name": "get_weather", "arguments": "{\"city\":\"Dhaka\"}"}
}]
},
"finish_reason": "tool_calls"
}]
}
Send the result back as the next turn, and the model answers normally:
"messages": [
{"role": "user", "content": "What is the weather in Dhaka?"},
{"role": "assistant", "content": null, "tool_calls": [ ... ]},
{"role": "tool", "tool_call_id": "call_5f1c9a2b7e0d4c8fa3b61d92", "content": "{\"temp_c\": 31}"}
]
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, …). |
tools, tool_choice, parallel_tool_calls | array · string · boolean | Function calling. Calls come back as function_call output items (and response.function_call_arguments.done when streaming); replay them plus your function_call_output items in input to continue the loop. |
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.
Qwen models & tools
Spark can also proxy Alibaba Qwen (the chat.qwen.ai web models) as a
separate backend. Connect a Qwen account on the Qwen page of the dashboard (sign in with your
Qwen email + password — the session token is minted and auto-refreshed for you), then create an API
key linked to that Qwen account. That key routes every endpoint below to Qwen. No code change
is needed — it's the same OpenAI/Anthropic-compatible API.
Models
GET /v1/models returns your account's live Qwen models (e.g. qwen3.7-plus,
qwen3.7-max, qwen3-coder-plus, qwen3-vl-plus). Use those ids
directly. For Anthropic clients (Claude Code, SDKs), the usual opus/sonnet/haiku
names map onto Qwen tiers automatically.
Tools via model-name suffix
Append a suffix to the model name to select a Qwen tool. With no suffix you get plain text chat.
| Suffix | Tool | Notes |
|---|---|---|
-thinking | Reasoning mode | Enables the model's think phase. |
-search | Web search | Answer cites live web sources; a Sources list is appended. |
-image | Image generation | Also available via /v1/images/generations. Returns a public image URL. |
-image-edit | Image editing | Attach an input image (see Vision input). |
-web-dev | Web Dev | Returns a complete self-contained HTML page. |
-deep-research | Deep research | Multi-minute; streams a research trace then a report. |
-video | Video generation | Async; often 10–20 min, which can exceed the request window. |
Text
curl https://spark.payfara.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"model":"qwen3.7-plus",
"messages":[{"role":"user","content":"Explain the CAP theorem in two sentences."}]}'
Image generation
Either add the -image suffix to the chat model, or use the Images API. Qwen image URLs are public.
curl https://spark.payfara.com/v1/images/generations \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"model":"qwen3.7-plus","prompt":"a red bicycle on a beach at sunset"}'
{ "created": 1783600000,
"data": [{ "url": "https://cdn.qwenlm.ai/output/…/t2i/….png?key=…" }] }
Web search
curl https://spark.payfara.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"model":"qwen3.7-plus-search",
"messages":[{"role":"user","content":"What are today’s top AI headlines?"}]}'
Web Dev (HTML generation)
curl https://spark.payfara.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"model":"qwen3.7-plus-web-dev",
"messages":[{"role":"user","content":"A landing page for a coffee shop called Bean There."}]}'
Vision input (images/documents)
Send an image the standard OpenAI way; Spark uploads it to Qwen and references it for you. Use a vision model such as qwen3-vl-plus.
curl https://spark.payfara.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"model":"qwen3-vl-plus","messages":[{"role":"user","content":[
{"type":"text","text":"What is in this image?"},
{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0…"}}
]}]}'
Video generation
Add -video to the model. Qwen video is an asynchronous job that commonly
takes 10–20 minutes — longer than a single request can stay open. The request returns a
task id immediately (with stream:true it also polls for a few minutes and returns
the URL if it finishes in time). Poll for the finished video with the task id:
# start the job
curl https://spark.payfara.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"model":"qwen3.7-plus-video","messages":[{"role":"user","content":"a cat playing piano"}]}'
# → assistant content includes: Task id: `abc-123`
# poll until status is "success"
curl https://spark.payfara.com/v1/videos/abc-123 -H "Authorization: Bearer $KEY"
# → {"task_id":"abc-123","status":"running"} … then {"status":"success","video_url":"https://…"}
Anthropic / Claude Code
A Qwen-linked key also serves POST /v1/messages. Claude Code works out of the box:
export ANTHROPIC_BASE_URL=https://spark.payfara.com export ANTHROPIC_API_KEY=$KEY # a Qwen-linked Spark key claude # sonnet/opus/haiku map to Qwen tiers
Qwen tools depend on your Qwen account's plan/quota; heavy tools (image/video/deep-research) have daily limits and, like the Gemini backend, work most reliably from a clean egress IP.
ChatGPT (GPT-5) models
Spark can proxy OpenAI's GPT-5 family through your own ChatGPT account —
no platform.openai.com API key and no per-token billing. Connect an account on the
ChatGPT page of the dashboard, then create an API key linked to it. That key routes
every endpoint below to ChatGPT.
This works on the Free ChatGPT plan, as well as Go, Plus, Pro, Business, Edu and Enterprise.
Your ChatGPT plan's own usage limits apply; when you exhaust them the API returns
429 with the reset time.
OpenAI meters these accounts as a percentage of a rolling window — typically a short (5-hour) window plus a weekly cap, both of which must have room — rather than as a fixed number of requests. Bigger models and longer prompts consume the window faster, so there is no "requests remaining" figure. The ChatGPT page in the dashboard shows the live percentage used and the reset time for each window.
Connecting an account
Login uses OpenAI's device-code flow, so there is nothing to paste back and no
localhost redirect. Click Connect with ChatGPT, enter the short code shown at
auth.openai.com/codex/device, approve, and the dashboard finishes automatically.
Only the resulting OAuth token is stored, and it is refreshed for you.
Models
GET /v1/models returns the model slugs your account may use — e.g.
gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna,
gpt-5.5, gpt-5.4, gpt-5.4-mini. Familiar OpenAI names
(gpt-4o, gpt-4, o3, …) are aliased onto these automatically, so
existing client code keeps working.
curl https://spark.payfara.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"model":"gpt-5.5","messages":[{"role":"user","content":"Explain CRDTs briefly."}]}'
Reasoning effort
Append an effort suffix to the model name to control how long the model thinks.
curl https://spark.payfara.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"model":"gpt-5.5-high","messages":[{"role":"user","content":"Prove it."}]}'
Valid suffixes: -minimal, -low, -medium, -high, -xhigh, -max.
Responses API
The ChatGPT backend speaks the OpenAI Responses API natively, so
POST /v1/responses (used by n8n's "Message a model") passes straight through.
curl https://spark.payfara.com/v1/responses \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"model":"gpt-5.5","input":"Write a haiku about databases."}'
Anthropic / Claude Code
A ChatGPT-linked key also serves POST /v1/messages:
export ANTHROPIC_BASE_URL=https://spark.payfara.com export ANTHROPIC_API_KEY=$KEY # a ChatGPT-linked Spark key claude # opus/sonnet/haiku map to GPT-5 tiers
Vision (image inputs) is supported. Image generation, audio and embeddings are not available on this backend — use the Gemini or Qwen backends for those.
duck.ai models & images
Spark can proxy duck.ai (DuckDuckGo's AI chat) as its own backend. It's the odd one out: duck.ai has no account — no sign-up, no password, no API key. What identifies a caller is a short-lived session your browser mints while you use the site.
So a duck.ai connection is made from the Spark Connect extension, not from a form:
open duck.ai, send one message, then click Connect duck.ai in the extension. It
reads the session out of your own browser and hands it to Spark, which replays it. Spark never
fabricates a session; when yours stops being accepted, you reconnect.
Sessions age out in minutes, and only a browser can mint a new one. After you connect,
the extension keeps watching duck.ai and forwards each new session automatically, so a key stays alive
for as long as you keep using duck.ai yourself. Leave it alone for a while and the connection flips to
expired — send one message on duck.ai to bring it back. This is the one Spark backend whose
keys are not fire-and-forget.
Models
GET /v1/models returns duck.ai's live catalog (e.g. gpt-5.6-luna,
gpt-5.4-mini, claude-haiku-4-5, mistral-small-2603), plus the
tool variants below. The default — used for chat and for image generation whenever a request
doesn't name a model Spark recognises — is gpt-5.6-luna. Claude tier names
(opus/sonnet/haiku) map onto the closest live model.
Not every listed model is usable. DuckDuckGo publishes its whole catalog but
reserves some models (gpt-5.4, claude-sonnet-4-6,
claude-opus-4-8) for its paid subscribers. Those are marked
“paid tier, unavailable” in the model list and are never chosen for you; asking for one
by name returns 403 naming the models you can reach, rather than duck.ai's bare
404 ERR_MODEL_RESTRICTED. If your DuckDuckGo session is a subscribing one, they
simply list as available.
Tools by model suffix
Append a suffix to the model name. With no suffix you get plain text chat.
| Suffix | What it does |
|---|---|
-image | Generate an image (duck.ai's GenerateImage tool) |
-search | Allow web / news / local / weather search |
-image-search | Both |
curl https://spark.payfara.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"model":"gpt-5.6-luna","messages":[{"role":"user","content":"Explain CRDTs briefly."}]}'
Image generation
Either add -image to the chat model, or use the Images API. duck.ai returns images
inline as base64, so b64_json is the default response format — there is no hosted URL
to link to (asking for url returns a data: URI).
curl https://spark.payfara.com/v1/images/generations \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"prompt":"A lush green canopy in Jamuna Eco Park at golden hour"}'
A duck.ai connection is a browser session, not an API key. DuckDuckGo authenticates
a caller with a browser-check proof that ages out in minutes and that only a real browser can mint,
so the Spark Connect extension renews yours automatically every few minutes — connect once and the
key keeps working, as long as the browser you connected from is running. When duck.ai does ask for a
new check (usually because several requests hit one connection at the same moment), Spark asks the
extension for a fresh session, waits for it and retries, so the call normally just runs a few
seconds late. If none arrives you get 429 with Retry-After and the session
age; the connection is not disabled. Image generation is the most sensitive to this and
takes ~30 s a picture, so prefer sequential calls, and connect a second browser if you need
real parallelism — a key rotates across every duck.ai connection you own.
Anthropic / Claude Code
A duck.ai-linked key also serves POST /v1/messages:
export ANTHROPIC_BASE_URL=https://spark.payfara.com export ANTHROPIC_API_KEY=$KEY # a duck.ai-linked Spark key claude
Not available on this backend: image input (vision), image editing, embeddings, and audio — duck.ai's chat endpoint accepts text only. Use a Gemini or Qwen key for those.
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 |
Plans & limits
Every key belongs to an account on a plan, and the plan decides three things:
how many requests a month you may make, which upstream backends your keys may
route to, and which capability endpoints are switched on. Limits are set per
plan in the dashboard, so the numbers below are the shipping defaults rather
than hard-coded rules — your live allowance is always at
Billing or GET /api/plan.
| Free | Pro — ৳100/mo | |
|---|---|---|
| Requests / month | 2,000 | Unlimited |
| Google Gemini accounts | 1 | 5 |
| ChatGPT accounts | 1 | 3 |
| Qwen accounts | 1 | 3 |
| Antigravity accounts | — | 2 |
| duck.ai connections | 1 | 3 |
| Active API keys | 3 | Unlimited |
| Image generation | Yes | Yes |
| Embeddings, audio, video | — | Yes |
Two failure modes are worth handling in client code. A plan that does not
include an endpoint or a backend returns 403; running out of
monthly requests returns 429. Both carry a human-readable
error.message naming the plan and what it lacks, and both are
resolved by upgrading rather than retrying.
{ "error": { "message": "You've reached your Free plan limit of 2000 requests this month. Upgrade for unlimited access.", "type": "insufficient_quota" } }
Paid plans are billed monthly with bKash. The first payment authorises a tokenized agreement, after which renewing is a single PIN entry — there is no silent auto-debit, so a lapsed plan keeps working for a short grace period rather than cutting off mid-request.
Account rotation
A key names one account, but it is not limited to it. When that account
reports a quota or rate limit, the request automatically falls through to
the next account you have connected on the same backend, and the
limited one is parked for a while so later requests skip it instead of
re-discovering the limit. Connect three ChatGPT accounts and a single
sk-gpt-… key draws on all three.
| Rule | Behaviour |
|---|---|
| Default account | The one chosen when the key was created. Always tried first while it's healthy. |
| Same backend only | A Gemini key reaches Gemini accounts, a Qwen key Qwen accounts, and so on. Backends are never mixed. |
| Same owner only | The pool is your own connected accounts — never another user's. |
| When it triggers | Quota exhausted, rate limited, or the account's sign-in was rejected. A malformed request fails immediately instead of burning the pool. |
| Streaming | Failover happens before the first token. Once output has started, a later failure is reported as-is rather than restarting on another account. |
| Pool exhausted | The upstream error is returned with the rotation noted, e.g. "…(tried all 3 of your ChatGPT accounts — each is limited or unavailable)". |
Resting accounts are labelled in the dashboard and recover on their own — for ChatGPT the timer follows the reset that OpenAI reports, elsewhere it's a short fixed window. Because rotation is per backend, the way to raise a key's ceiling is to connect more accounts of that same backend, up to your plan's cap.
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 |
403 | Your plan doesn't include this backend or endpoint (see Plans & limits) |
429 | Monthly plan quota exhausted, or the upstream account's own usage limit (Gemini 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.
Tool use is supported on every backend: send tools with
input_schema and calls come back as tool_use content blocks with
stop_reason: "tool_use"; reply with tool_result blocks to continue.
tool_choice accepts {"type":"auto"|"any"|"tool","name":"…"}. The same
emulation caveats as function calling apply — streaming buffers while tools
are active, and forcing a call is best-effort.
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.