On this page
KiwiMate API reference
The KiwiMate API gives your apps the same models that power KiwiMate, through a single endpoint that follows OpenAI's chat.completions format. If your code already talks to OpenAI, you usually only need to change the base URL, the API key and the model name.
https://gznrhppouxwpfihlfgpb.supabase.co/functions/v1/api-chat-completionsSend requests to the base URL itself. Requests to {base URL}/chat/completions (the path the OpenAI SDKs add) reach the same endpoint, so either works. All prices are in New Zealand dollars.
Quickstart
1. Create a key. Sign in and open the developer console, then click Create new key. Your first key comes with $5 NZD of trial credit.
2. Store it in an environment variable. Every example in these docs reads it from KIWIMATE_API_KEY.
export KIWIMATE_API_KEY="sk-km-..."
# Optional: the OpenAI SDK for your language
pip install openai # Python
npm install openai # Node.js3. Make a request.
curl https://gznrhppouxwpfihlfgpb.supabase.co/functions/v1/api-chat-completions \
-H "Authorization: Bearer $KIWIMATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kiwimate-medium-1-0",
"messages": [
{
"role": "user",
"content": "Kia ora! Suggest three day trips from Wellington."
}
]
}'Prefer to experiment first? The playground lets you try every model and setting in the browser, then copy the matching code.
Authentication
Authenticate every request with your API key as a bearer token. Keys start with sk-km-.
Authorization: Bearer sk-km-...- Keys are shown once, when you create them. KiwiMate only stores a hash, so a lost key can't be recovered. Revoke it and create a new one.
- Revoking a key in the console takes effect immediately. Revoked, unknown or missing keys get
401 authentication_error. - All keys on your account spend from the same credit balance. Create one key per app or environment so you can revoke them independently and see per-key spend in the console.
Keep keys secret
Models
Pass one of these ids as model. Prices are NZD per million tokens.
| Model | Vision | Thinking | Max output | Input | Output |
|---|---|---|---|---|---|
| kiwimate-mini-1-0 | Yes | No | 1,024 | $1 | $3 |
| kiwimate-small-1-0 | Yes | Yes | 1,024 (3,072 thinking) | $2 | $5 |
| kiwimate-medium-1-0 | Yes | Yes | 1,024 (3,072 thinking) | $4 | $8 |
| kiwimate-large-1-0 | Yes | Yes | 1,024 (3,072 thinking) | $5 | $10 |
- Any other model id returns
400 invalid_request_error, including the retiredkiwimate-mini-previewandkiwimate-small-preview. - Image generation models (KiwiMate-Image-Mini-1.0, KiwiMate-Image-Fast-1.0, KiwiMate-Image-Pro-1.0) aren't available through the API yet. These docs will cover them once they are.
- Model sizes, licences and open-weight downloads are on the models page.
List models
GET {base URL}/models returns the models above in OpenAI's format, so client.models.list() works and tools that check your key or fetch the model list on startup can connect. It needs a valid key, but it's free and doesn't count toward your rate limit.
GET https://gznrhppouxwpfihlfgpb.supabase.co/functions/v1/api-chat-completions/models
Authorization: Bearer sk-km-...{
"object": "list",
"data": [
{ "id": "kiwimate-mini-1-0", "object": "model", "created": 1787097600, "owned_by": "kiwimate" },
{ "id": "kiwimate-small-1-0", "object": "model", "created": 1787097600, "owned_by": "kiwimate" },
{ "id": "kiwimate-medium-1-0", "object": "model", "created": 1787097600, "owned_by": "kiwimate" },
{ "id": "kiwimate-large-1-0", "object": "model", "created": 1787097600, "owned_by": "kiwimate" }
]
}Chat completions
Creates a model response for the given conversation.
POST https://gznrhppouxwpfihlfgpb.supabase.co/functions/v1/api-chat-completions
Authorization: Bearer sk-km-...
Content-Type: application/jsonRequest body
| Parameter | Type | Description |
|---|---|---|
model | string, required | A model id from the models table. |
messages | array, required | The conversation so far, oldest first. Must be non-empty, and the last message must have role: "user". See Messages. |
stream | boolean | Default false. Stream the reply as server-sent events. See Streaming. |
max_or max_tokens | integer > 0 | Upper limit on generated tokens. Send either name. max_tokens is the older one, and if you send both, max_ is used. Values above the model's ceiling (1,024, or 3,072 with thinking on) are lowered to the ceiling instead of being rejected. Defaults to the ceiling. |
temperature | number, 0–2 | Default 0.7. Lower is more focused, higher is more varied. 0 is effectively deterministic. |
thinking | boolean | KiwiMate extension. Default false. Ask the model to reason before answering. See Thinking. |
Other OpenAI parameters (for example tools, response_format, n, top_p, stop, seed and stream_options) are accepted but ignored.
Messages
Each message is { role, content }. role is user, assistant or system. content is either a string or an array of content parts (text and image_url, see Vision). Other roles such as developer or tool return 400.
System messages and history
system messages are accepted for compatibility but are not sent to the model. To steer behaviour, put your instructions in the first user message. Only the most recent 20 user and assistant messages are sent to the model, and older ones are dropped.Vision
Every model accepts images alongside text. Send them as image_url content parts containing a base64 data:image/… URL. Remote https:// image URLs aren't supported, so download the image and encode it first.
curl https://gznrhppouxwpfihlfgpb.supabase.co/functions/v1/api-chat-completions \
-H "Authorization: Bearer $KIWIMATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kiwimate-medium-1-0",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "What native bird is in this photo?"
},
{
"type": "image_url",
"image_url": {
"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
}
}
]
}
]
}'- At most 4 images per message.
- Each data URL must be under 3,000,000 characters, about 2.2 MB of image data.
- Images count toward input tokens, so larger images cost more. Resize them to the smallest size that still works for your task.
Thinking
Set thinking: true to have the model reason step by step before it answers. Useful for maths, logic and multi-step questions. Supported by kiwimate-small-1-0, kiwimate-medium-1-0 and kiwimate-large-1-0. Sending it to a model without thinking support returns 400.
curl https://gznrhppouxwpfihlfgpb.supabase.co/functions/v1/api-chat-completions \
-H "Authorization: Bearer $KIWIMATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kiwimate-large-1-0",
"messages": [
{
"role": "user",
"content": "A ferry leaves Wellington at 8:45am and takes 3h 30m to reach Picton. When does it arrive?"
}
],
"thinking": true,
"max_tokens": 2048
}'- The reasoning comes back in
message.reasoning_content, separate frommessage.content. When streaming it arrives indelta.reasoning_content. - Thinking tokens are billed as output tokens. The output ceiling rises to 3,072 tokens to make room for them.
- If the model uses up its token limit while still thinking,
contentholds a short note saying it ran out of room andfinish_reasonis"length". Raise the limit or turn thinking off for that request.
Streaming
With stream: true, the response is text/event-stream. Each event is a data: line holding a chat.completion.chunk, and the stream ends with data: [DONE]. Tokens arrive as the model generates them, and every chunk in a response shares the same id.
curl -N https://gznrhppouxwpfihlfgpb.supabase.co/functions/v1/api-chat-completions \
-H "Authorization: Bearer $KIWIMATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kiwimate-small-1-0",
"messages": [
{
"role": "user",
"content": "Write a haiku about kiwifruit."
}
],
"stream": true
}'data: {"id":"chatcmpl-…","object":"chat.completion.chunk","model":"kiwimate-small-1-0","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","model":"kiwimate-small-1-0","choices":[{"index":0,"delta":{"content":"Fuzzy"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","model":"kiwimate-small-1-0","choices":[{"index":0,"delta":{"content":" brown"},"finish_reason":null}]}
…
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","model":"kiwimate-small-1-0","choices":[{"index":0,"delta":{"content":""},"finish_reason":"stop"}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","model":"kiwimate-small-1-0","choices":[],"usage":{"prompt_tokens":31,"completion_tokens":19,"total_tokens":50}}
data: [DONE]- The last chunk before
[DONE]carriesusageand has an emptychoicesarray. Check thatchoices[0]exists before reading it. - With thinking on, reasoning streams in
delta.reasoning_contentfirst, anddelta.contentmay be an empty string until the answer starts. - You're charged once per request, from the model's token count. Disconnecting early doesn't cancel the charge: you pay for what the model processed up to that point, meaning the prompt plus the tokens generated so far (which can include a few produced just after you disconnected). In the rare case the model hasn't reported a count yet, the charge is estimated at about 4 characters per token.
Response format
Non-streaming requests return a standard chat.completion object.
{
"id": "chatcmpl-3f1c2a9e-8d7b-4c1e-9a55-0e6f7b2d1c44",
"object": "chat.completion",
"created": 1790000000,
"model": "kiwimate-medium-1-0",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Kia ora! Here are three great day trips from Wellington: ...",
"reasoning_content": "..."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 38,
"completion_tokens": 212,
"total_tokens": 250
}
}reasoning_contentis only present when you asked for thinking and the model produced some.usageholds exact token counts reported by the model server. These are the numbers you're billed on.finish_reasonis"length"when the reply was cut short by the token limit, and"stop"when the model finished on its own. Streaming responses report it the same way, in the last chunk that has a choice.
Errors
Errors use OpenAI-style status codes and bodies, so the OpenAI SDKs' built-in error classes and retries work as expected.
{
"error": {
"type": "invalid_request_error",
"message": "temperature must be a number between 0 and 2"
}
}| Status | error.type | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_request_error | Bad JSON, unknown model, invalid messages, unsupported thinking, too many or too-large images, or out-of-range parameters. | Fix the request. The message says what's wrong. |
| 401 | authentication_error | Missing, malformed, unknown or revoked API key. | Check the Authorization header and key. |
| 405 | invalid_request_error | Method other than POST, apart from GET on /models. | Use POST, or GET for /models. |
| 429 | rate_limit_exceeded | More than 60 requests this minute on this key. | Wait for the next minute, then retry with backoff. |
| 429 | insufficient_quota | Your credit balance is zero or below. | Add credit. Retrying won't help. |
| 502 | server_error | The model server returned an error or an empty reply. | Retry once or twice with backoff. |
| 503 | server_error | The model was still starting up after 45 seconds. | Retry after the Retry-After header (30 s). |
Failed requests are never charged. Both 429 types share a status code, so check error.type to tell them apart. The OpenAI SDKs retry every 429 automatically, including insufficient_quota, so an empty balance surfaces after a few seconds of retries rather than straight away.
Cold starts
Models scale down when idle. The first request after a quiet spell waits while the model wakes up, up to 45 seconds, and returns 503 if it's still not ready. Set your HTTP client's timeout above 60 seconds so you don't give up early.
import os
from openai import OpenAI, RateLimitError
# The OpenAI SDK retries 429 and 5xx responses for you (2 retries by default,
# honouring Retry-After). A few more retries help ride out a cold start.
client = OpenAI(
api_key=os.environ["KIWIMATE_API_KEY"],
base_url="https://gznrhppouxwpfihlfgpb.supabase.co/functions/v1/api-chat-completions",
max_retries=4,
)
try:
completion = client.chat.completions.create(
model="kiwimate-medium-1-0",
messages=[{"role": "user", "content": "Kia ora!"}],
)
except RateLimitError as err:
if err.type == "insufficient_quota":
print("Out of credit. Top up in the KiwiMate developer console.")
else:
raiseRate limits
- Each key can make 60 requests per minute. The count resets at the start of every clock minute, so it isn't a rolling 60-second window.
- Every chat completion request with a valid key counts, including ones later rejected for a bad body or an empty balance. Listing models doesn't count.
- Limits apply per key. Need more? Email hello@kiwimate.net and tell us about your use case.
Pricing & billing
The API is pay-as-you-go from a prepaid NZD credit balance. There are no subscriptions or monthly minimums.
- Per-token pricing. Each request costs
input tokens × input price + output tokens × output price, using the per-million prices in the models table. - Rounded up to the cent. Each request's cost is rounded up to a whole cent, so every successful request costs at least $0.01. For many small requests, this minimum is most of the cost.
- Trial credit. Your first API key adds $5 NZD of credit to your account, once per account.
- Top-ups. Add $1–$1,000 NZD at a time by card from the console. Payments are processed by Stripe.
- Balance checks. Requests are accepted while your balance is above zero and charged when they finish, so your final request can take the balance slightly below zero. Later requests get
429 insufficient_quotauntil you top up. - Tracking spend. The console shows daily spend, usage by model and key, and every charge, which you can download as CSV.
OpenAI SDKs
The official OpenAI libraries for Python and Node.js work unchanged. Set the base URL to KiwiMate's and use your KiwiMate key.
- Python:
OpenAI(api_key=..., base_url="https://gznrhppouxwpfihlfgpb.supabase.co/functions/v1/api-chat-completions"). Pass KiwiMate-only fields throughextra_body={"thinking": True}, and readgetattr(message, "reasoning_content", None). - Node.js:
new OpenAI({ apiKey, baseURL: "https://gznrhppouxwpfihlfgpb.supabase.co/functions/v1/api-chat-completions" }). In TypeScript,thinkingandreasoning_contentaren't in the SDK's types, so add a cast or a// @ts-expect-errorcomment where you use them. client.models.list()returns the API's models, and chat calls work whether they setmax_completion_tokensor the oldermax_tokens.- Other OpenAI-compatible tools that let you set a custom base URL and model name should work too, as long as they only use the features on this page.
Best practices
- Keep keys on the server, in environment variables or a secrets manager. Rotate a key if you think it has leaked.
- Stream responses in chat interfaces so people see the reply as it's written instead of waiting for the whole thing.
- Set
max_tokensto what you actually need, to cap both cost and latency. - Retry
429 rate_limit_exceeded,502and503with exponential backoff. Don't retry400,401orinsufficient_quota. - Pick the smallest model that does the job well. Mini is ideal for short, simple tasks, and Large for the hardest ones.
- Check the console regularly, and top up before your balance runs out if you run anything in production.
Current limitations
The KiwiMate API is young. These are the things it doesn't do yet:
- No custom system prompts. The KiwiMate persona is always applied (see Messages).
- No tool/function calling, JSON mode, logprobs or multiple choices (
n). - No embeddings endpoint, and no single-model lookup (
GET /models/{id}). The only endpoints are chat completions and listing models. - No image generation through the API yet.
- Only the last 20 messages of a conversation reach the model.
- Replies stop early if the model writes the literal text
User:orAssistant:, which are used internally as stop sequences.
Have a request or found a bug? Tell us on the Ideas & Progress board or report an issue.