spark

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:

MethodPathAuthDescription
POST/v1/chat/completionsBearer keyChat completion (streaming + non-streaming, text + images)
POST/v1/responsesBearer keyOpenAI Responses API (used by n8n's OpenAI node, newer SDKs)
POST/v1/images/generationsBearer keyGenerate image(s) from a prompt (Gemini native)
POST/v1/images/editsBearer keyEdit an image with a prompt (multipart; img-to-img)
POST/v1/audio/transcriptionsBearer keyTranscribe an audio file (emulated via Gemini)
POST/v1/audio/translationsBearer keyTranscribe + translate audio to English (emulated)
POST/v1/moderationsBearer keyClassify text for policy violations (emulated)
POST/v1/embeddingsBearer keyText embeddings via the official Google AI API (needs a Google AI key)
POST/v1/messagesx-api-key or BearerAnthropic Messages API — Claude model IDs mapped to Gemini
GET/v1/modelsBearer keyList models available to the key's account
GET/v1/models/{id}Bearer keyRetrieve a single model
GET/v1/images/proxySigned URLServes generated images (used internally by responses)
GET/healthznoneHealth 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

FieldTypeRequiredDescription
modelstringnoModel slug, display name, internal id, or alias. See /v1/models. Defaults to the account's default model.
messagesarrayyesList of {role, content}. Roles: system, user, assistant, tool. Multi-turn is flattened into one prompt.
streambooleannoIf true, responds with server-sent events. Default false.
temperature, max_tokens, usernoAccepted 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

FieldTypeDescription
modelstringSame resolution as chat completions (slug / display name / internal id / alias).
inputstring · arrayA plain string, or an array of message items {role, content} where content is a string or content parts (input_text, etc.).
instructionsstringOptional system prompt, prepended to the conversation.
streambooleanIf 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": "![generated image 1](https://spark.payfara.com/v1/images/proxy?u=…&a=2&s=…)"
    },
    "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.

QueryDescription
ubase64url-encoded upstream image URL
aaccount id
sHMAC 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.

MethodPathDescription
GET/api/meCurrent user
GET / POST/api/accountsList / add a Gemini account (cookies)
POST/api/accounts/:id/testRe-verify an account
DELETE/api/accounts/:idDelete an account
GET / POST/api/keysList / create proxy API keys
DELETE/api/keys/:idRevoke a key
GET/api/usageUsage summary
GET/api/modelsModels for the playground
POST/api/playground/chatPlayground generation (custom SSE)
/auth/login, /auth/callback, /auth/logoutGoogle OAuth flow

Errors

Errors follow the OpenAI error envelope:

{ "error": { "message": "Invalid API key.", "type": "authentication_error" } }
StatusMeaning
401Missing/invalid bearer key
400Bad request body, or no Gemini account configured for the key
429Gemini usage limit exceeded (code 1037)
502Gemini 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

FieldTypeDescription
inputstring · string[]Text to embed (single or batch).
modelstringgemini-embedding-001 (default), gemini-embedding-2, text-embedding-004; OpenAI names like text-embedding-3-small map to a default.
dimensionsnumberOptional output dimensionality (e.g. 768) for models that support truncation.
encoding_formatstringfloat (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/modelsWhat you send in model fieldRouted to
claude-gemini-3-flashclaude-gemini-3-flashgemini-3-flash
claude-gemini-3-proclaude-gemini-3-progemini-3-pro
claude-gemini-3.1-flash-liteclaude-gemini-3.1-flash-litegemini-3.1-flash-lite
claude-gemini-proclaude-gemini-progemini-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 sentKeyword matchedRouted to
claude-3-5-sonnet-20241022contains sonnetgemini-3-flash
claude-sonnet-4-6contains sonnetgemini-3-flash
claude-opus-4-7contains opusgemini-3-pro
claude-3-opus-20240229contains opusgemini-3-pro
claude-haiku-4-5contains haikugemini-3-flash
claude-3-5-haiku-20241022contains haikugemini-3-flash
anything elsedefaultgemini-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"):

  1. Create an OpenAI credential. Set API Key to your sk-gem-… proxy key.
  2. Set Base URL to https://spark.payfara.com/v1.
  3. 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:

ResourceOperationStatus
TextMessage a Model✅ supported (native)
TextClassify Text for Violations✅ supported (emulated)
ImageAnalyze Image✅ supported (vision)
ImageGenerate an Image⚠️ supported (native, intermittent — retry on 502)
ImageEdit an Image⚠️ supported (native, intermittent)
AudioTranscribe a Recording✅ supported (emulated)
AudioTranslate a Recording✅ supported (emulated)
AudioGenerate Audio (TTS)❌ not supported (no Gemini TTS) → 501
AssistantCreate / Update / Delete / List❌ not implemented (Assistants API)
AssistantMessage an Assistant❌ not implemented (Threads/Runs)
FileUpload / 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.