guide

Errors

When something fails, your app gets a code to act on and a message you can show people. Every failed call throws a GemmeinError with a stable code to branch on and a message that reads correctly for people.

import { GemmeinError } from "@gemmein/sdk"

try {
  // "tasks" must exist first: a new app has no collections (404 unknown_collection)
  await g.collection("tasks").create({ title: "Test" })
} catch (err) {
  if (err instanceof GemmeinError) {
    err.code      // branch on this
    err.status    // HTTP status
    err.message   // human-readable — render it; it includes what to do next
    err.resetAt   // on 429s — when to retry (absent on storage-space 429s)
  }
}

forbidden vs denied

forbidden (403) means the safety rules refused the request. The same call will always be refused, so change the approach. denied is retriable or fixable: a rate limit (429, so wait for resetAt) or a missing sign-in (401, so sign in). Distinguish by HTTP status. Rate-limit 429s carry resetAt; the storage-space 429 (usage_limit_exceeded on upload) carries none, because that limit is on stored bytes; deleting files lifts it.

entitlement_required is the exception. It is the one 403 that can succeed later: the person is signed in and doesn't hold the plan or product this collection is unlocked by. It carries that plan's key on the error (err.requires, e.g. "access:pro" for a plan named pro; one key, a string, even when several plans open the collection), so your app can show an upgrade prompt instead of a dead end. You don't need to check before every read: let the call fail and handle it.

The codes

Keys and domains

CodeStatusMeaning · what to do
missing_app_key401No app key; get one at app.gemmein.com
invalid_app_key403Key not recognized: typo, or wrong environment
origin_not_allowed403The request came from an address this key does not allow. The Development app key works from any secure (https) address, such as a browser builder's preview, a host's preview deploy or your own domain, and from localhost, before and after go-live, with nothing to list. The Live app key works only on the app's verified domains. A Live key on a local page is told: "Your Live app key refuses local pages; work locally with your Development app key or npx gemmein dev." A Development key on any other local address is told: "With a Development app key, local pages work only on localhost or 127.0.0.1, over http or https." Plain http on a public name, a file or a sandboxed frame is told: "A Development app key works from any secure (https) address, and over plain http only on localhost or 127.0.0.1." On npx gemmein dev, any other named address is told: "npx gemmein dev allows pages on http://localhost and http://127.0.0.1 only. To call from a public https address, use your cloud Development app key (npx gemmein keys app prints it)." The body carries reason (no_key, secret_key, unknown_key, local, address, domain or insecure) to branch on, never the sentence, and, once the key names an app, live (true once the app is live) and environment (development or production: whose rule refused the page). A key problem is about the key, whatever the page's address. With no key: "This request has no app key. Add the app key npx gemmein keys app prints." With a secret key: "A secret key never goes in a browser. Use the app key npx gemmein keys app prints." With a key Gemmein doesn't know: "This app key isn't recognised. Use the app key npx gemmein keys app prints." On npx gemmein dev the same three point at the key it prints instead: "This request has no app key. Add the app key npx gemmein dev prints (pk_local_…)." "A secret key never goes in a browser. Use the app key npx gemmein dev prints (pk_local_…)." "This app key isn't recognised by gemmein dev. Use the app key npx gemmein dev prints (pk_local_…)." Add and verify the domain the app goes live on (a host's preview address can't be added); never retry. A refused https address on the Live key shows on the dashboard's Errors page
local_key403A pk_local_ key from gemmein dev was used against the cloud; local keys only work against 127.0.0.1 while gemmein dev is running
live_paused503Live is paused: the Gemmein plan behind this app ended, or a failed payment stayed unpaid for 7 days. Every Live request answers it; Development keeps answering. To the people using the app (an app key): "This app is temporarily unavailable. Please try again later." To your own server code (a server key): "Your Gemmein subscription ended, so Live is paused. Development still works. Restart your plan from Settings → Usage & billing." After an unpaid payment the second sentence is "Your Gemmein payment didn't go through, so Live is paused. Development still works. Update your card from Settings → Usage & billing." Show your own temporarily-unavailable state and try again later; nothing your app does lifts it, only the founder paying. Webhooks sent to Live are kept (answered 200 with "held": true) and run in order when Live is back
live_unavailable403Rotating a server key or an AI tool's secret while Live is a dashboard action for now. Nothing was rotated. Use Settings → API keys → the key → Rotate, or Settings → AI tools → the tool → Rotate secret
scope_denied403Secret key used outside its dashboard-configured scope:
  • A scoped key called a collection it wasn't given, tried to write when it may only read, or tried to delete when only "deletes too" would allow it. A full key reaches every collection, including ones added later.
  • A secret key asked to sign in as the account's owner, or to link or sync. The CLI links and syncs after npx gemmein login, approved in the browser; npx gemmein sync --live asks for its own Live approval.
  • The Live push key (sync --live) asked for anything but relays and AI tools.
  • On g.ai.chat(): a secret key called it. g.ai.chat() is for the browser; a server calls the provider directly.
secret_key_expired403A key handed out by a browser approval has ended; the message names the instant. This machine's Development approval lasts 7 days: run npx gemmein login and approve it again. A Live approval is good for 10 minutes: run npx gemmein sync --live again, and it asks for a fresh one
key_name_taken409npx gemmein keys create: the environment already has a key with that name, or keys revoke named one that two keys share. Pick another --name, or revoke the one you mean on Settings → API keys
key_not_found404npx gemmein keys revoke: no working server key has that name in that environment. npx gemmein keys list shows the keys there are
invalid_key_plan400npx gemmein keys: the key's request could not be read (a collection or access level it doesn't know). Nothing was made; the message names the part, and your AI runs the command again
no_environment409The approval or key names an environment this app doesn't have: a Live key before the app is live, for one. Make a Development key, or take the app live first
domain_exists409npx gemmein domains add: the domain is already on this app. npx gemmein domains shows its DNS records and whether it is verified
store_error500A change did not reach Gemmein's store, or was made but could not be read back (adding a domain, for one). The message says whether anything was added. Refresh the page or try again
key_limit_reached409An environment holds at most 50 active secret keys, and making another was refused: “Revoke a key to make another”. Every active key is listed on Settings → API keys; revoke one you no longer use. Rotating a key is never refused
app_limit_reached409An account holds at most 10 apps, and making another was refused. Apps can't be deleted; write to Gemmein to raise the limit
app_limit_free409Your first app is free. Pick a plan for it on Settings → Usage & billing to add another
app_limit_plan_inactive409Your plan isn't active, so a new app was refused — every app you already have keeps working. Update your card in Stripe, then try again
app_paused402This app is paused because your plan isn't active. Update your card in Stripe to keep building
secret_key_public400Going live reads your live site, and it found a secret key on a public page or a same-origin script it loads. Move it to your server, redeploy, rotate the key on Settings → API keys, then go live again
secret_scan_failed400Going live checks the CLI scan on record, and it found a secret key in your build. Move it to your server, rebuild, rotate the key, then run npx gemmein check again
secret_scan_incomplete400Going live couldn't finish reading one of your verified domains for secret keys within its 15 s budget, and there's no CLI scan on record to fall back on. Run npx gemmein check in your project, or confirm in the dashboard that no secret key ships in your site (the confirmation is logged), then go live again
secret_scan_confirm_required400A mobile-only app has no website for Gemmein to scan when you go live. Run npx gemmein check on the built app, or confirm that no secret key ships in your app: in the dashboard, or with npx gemmein go-live --confirm-no-secret-key (the confirmation is logged)
invalid_domain400The domain can't be added: it isn't written as a domain name (lowercase, like fernhall.app, with no https:// or path), or it is a shared host such as vercel.app. Add a domain you own
preview_address400The domain is your host's preview address (my-app.vercel.app); Gemmein can't check it or send from it. Add the domain you'll go live on: its records and proof carry to Live, so you do them once
apex_has_cname409Replies were asked to arrive at the name itself on a subdomain that points at its host with a CNAME. A name that is a CNAME can’t receive mail itself. Nothing was changed; keep replies on mail.<name>
domain_check_unavailable503Domain checks aren't switched on for this Gemmein, so the domain check can't run. Nothing changed
email_domain_exists409The app already sends email from a domain. An app sends from one domain at a time: remove that one first (Domains → the domain → Email), then add the new one
email_domain_claimed409Another app already sends email from this domain. Remove it from that app first
invalid_email_domain400The domain couldn't be set up to send your app's email; the message says why. Check the spelling (no https://, no path) and add it again
email_domains_unavailable503This deployment has no email provider, so a custom sender domain can't be added here. The app keeps Gemmein's sender
resend_error502The email provider refused or didn't answer while setting up the domain's email. If it refused, nothing changed: try again in a minute. If it didn't answer, reload in a minute to see whether it went through
invalid_senders400npx gemmein emails sender (or set_sender_words): the senders plan is missing, empty, or names something other than account, support or news. Send the sender word for at least one of the three

Sign-in and sessions

CodeStatusMeaning · what to do
auth_expired401Session expired. The SDK auto-clears the token; retry or sign in again
denied429A rate limit; wait until resetAt, then try again
denied401Sign-in is missing for this call. Sign the person in, then try again
denied500An unexpected failure on the server. Nothing you sent caused it; try again later
authentication_required401Sign in before a plan's checkout (a plan is tied to an account), a product bought with an item note, or reading a subscription. A product without a note can be bought signed out
user_suspended403Sign-in: the code was right, but the owner suspended this person, so no session is made. It stays refused until the owner reactivates them in the dashboard; show the message
people_limit_reached403“This app isn’t taking new sign-ups right now.” Legacy only — current pricing has no people limit, so no app receives this code any more. People already signed up keep working
invalid_email400Not a valid email address
invalid_code400The sign-in code is wrong or past its 10 minutes; ask for a new one
email_send_failed503The sign-in code email could not be sent (the email provider refused or failed the send, or could not be reached). The person’s earlier code, if they have one, still works; the code in any email from this try that does arrive won’t work. Retryable: show "Try again in a moment" and let the person ask again. The answer is the same for every address, and the try does not count toward the 3 per 15 minutes when the provider was down, busy or unreachable, or the time ran out before the email was handed over. A provider that takes the email but does not answer in time is not this error: the start answers 202, and the code works when the email arrives
missing_params400A required field is absent or empty; the message names it (missing email). The SDK passes what you give it, so a blank form field reaches the API as missing — check it first
field_too_long400A sign-in field (email, code, the test-session fields) is over 500 characters; the message names the field
test_session_forbidden_live403testSession() only works in Development. Use an sk_dev_ key
protected_account403This account is protected and cannot be deleted; retrying cannot help
export_rate_limited429A person exports their own data once every 30 days, counted from their last export; the dashboard exports a person's data once a day. The message gives the date and, on the person's own export, says they can still ask the app's owner for their data. Only that refusal, the person's own within 30 days, carries ownerContact: the app's support address, or null when it has none. While an export of the same person is still being prepared, another is refused at once, and resetAt is a couple of minutes away. Try again after resetAt
audit_unavailable503A data export's row in Logs could not be written, so nothing was handed out. Try again
erase_unavailable503Deleting an account, with g.account.delete() or from the Customers page: Gemmein couldn't read the app's payments, which it needs to end the person's subscription in Stripe. Nothing was deleted and nothing changed in Stripe. Show the message and let them try again in a moment
confirm_required403g.auth.signOutDevice(id) on another device needs a sign-in within the last 10 minutes. Gemmein emailed the person a confirm code: show a code field, then call signOutDevice(id, { code }). A wrong code is invalid_code; calling again without a code sends a new one
posts_unpublished409A rule change that shows every record (to private, admin_write, addressed or direct) while some posts aren't published: waiting, sent back, taken down by their author, or a draft. "2 posts aren't published — publish or delete them first." Publish or delete them in Records, then change the rule
device_not_found404g.auth.signOutDevice(id): none of the person's signed-in devices has that id; it may already be signed out. g.auth.devices() lists the ones still signed in
invalid_device400g.auth.signOutDevice(id) was given something that isn't a device id. Pass the id from g.auth.devices(); it starts with sess_

Data

CodeStatusMeaning · what to do
unknown_collection404Collection doesn't exist. The owner creates it in the dashboard; don't retry
not_found404A record you can't see. Existence is never leaked; treat it as absent
forbidden403The safety rules refused this. Never retry; fix the approach or show the message
app_only403The collection is app_only: no app session reads or writes it, signed in or not, yours included. Don't retry; signing in changes nothing. Only the dashboard, your server (a secret key) and relays reach it. Have your server or a relay copy what people may see into a collection they can read
conflict409A keyed create, a floor or ceiling, or a stale ifVersion refused the write. This is expected: tell the person it was taken
collection_not_empty409Deleting a collection that still has active records; the records go first. This is expected
invalid_publish400published is an option on public rules only, and never a data field
moderation_required403The collection's posts wait for the owner's approval. Save without published: true; the owner publishes from the dashboard
author_not_visible403list({ author }) on private, addressed or admin_write. Drop author
invalid_author400author is "me" (signed in) or a person's user id
invalid_publishing400A collection's "publishing" is "authors publish" or "owner approves", and "owner approves" works on community. On public_read the setting is refused: only you write it, so there's nothing to approve. Fix the file
invalid_shape400The live collection has no field with this name. Send only the fields it has. A new field reaches the live collection when the owner goes live again and approves it (one answer per field). This also covers a file field given the wrong kind (a document where the field takes an image, or the reverse), and the message names which
unknown_record400A link points at a record that doesn't exist (live only); fix the id
invalid_since400since must be a strict ISO 8601 timestamp (pass the previous answer's watermark), and it fixes the order; drop sort
invalid_show_time400showFrom / showUntil isn't an ISO 8601 instant with a zone (2026-11-01T09:00:00Z), the day doesn't exist, showUntil isn't later than showFrom, null came on a create, or the collection is private or app_only (nobody else reads it). The message names which
not_rankable400list({ sort: { by } }) named a field the collection doesn't declare rankable. Add it to "rankable" in gemmein/collections/<name>.json (a number field)
invalid_sort400sort isn't "newest", "oldest", "updated" or { by, order }, or order isn't "desc" or "asc". The message names the fix
invalid_expand400expand on a field or safety rule with no link shape; join in memory instead
system_collection403Collections named starting __ are the platform's own; no app session and no key reads, writes, or deletes them. Name the collection your code needs, or (for a transcribe tool's audio) upload with g.ai.upload(tool, file) instead
invalid_key400A keyed create's key doesn't match the allowed characters or length (1-120 chars of letters, numbers, : _ . @ / -); on spendCredits(), the key is not text of up to 200 characters
invalid_filter400where takes at most 5 fields, and values must be a string, number or boolean
invalid_count400count() takes only where and search; list({ count: true }) is refused on a since delta
invalid_search400search is over 200 characters
invalid_precondition400ifVersion must be a positive integer; pass the version you read
invalid_op400An atomic op targets a field that isn't a number in the collection's shape
invalid_body400Record data must be a plain JSON object; on g.ai.chat(), the body must be the provider's JSON request object, nested at most 32 levels
payload_too_large / file_too_large413Over the size cap. The message states it; shrink it. A record's data is at most 32 KB (files go through uploads, never inline). On g.ai.run(): the inputs are over 64 KB; send less. On g.ai.chat(): the raw body is over 256 KB; shorten the conversation you send
invalid_name400A collection name is 2-63 lowercase letters, numbers or underscores, starting with a letter, and never a JavaScript built-in (constructor, prototype). Rename the collection
invalid_limit400limit isn't a whole number from 1 to 100. Ask for at most 100 and page with cursor; nothing is cut down for you
invalid_open_fields400A collection's "openFields" is refused: it is on private or admin_write, names a link, file or server-managed field, gives an operation Gemmein doesn't know, or has an empty "one of" list. The message names the fix; correct the collection file or Open fields on the collection's page in the dashboard's Data
invalid_rankable400A collection's "rankable" names more than 3 fields, a field twice, or a name that isn't a data field; on community, shared and direct, a field that isn't a declared number. Fix the collection's file; the message names the field
not_open_field400open(id) named a field that isn't open; only the author changes it, with update(). The message lists the open fields. To let readers change it, name it under "openFields" in the collection file
wrong_open_operation400open(id) used the wrong operation for the field: a counter takes count(field, 1) or -1, a "one of" takes set(), a "one per person of" takes choose(), a flag takes flag(field, true) or false. The message names the right call
open_value_not_in_list400A "one of" or "one per person of" field was given a value that isn't one of its choices, or a choice taken off its list; the message lists them. Send a listed choice
open_field_write400App code wrote an open field through create() or update(). Open fields change through open(id); a secret key and the dashboard may set a "one of" to a listed choice, and counters and flags are never written directly
open_field_not_allowed403open(id) on a private or admin_write collection, where nobody else can change a field. Open fields work on shared, community, public_read, addressed and direct
open_field_needs_person403A secret key called open(id). Each like, vote or flag is a signed-in person's own; your server sets a "one of" with update(), and counters and flags are never written for someone
invalid_stats400stats(field) named something that isn't a data field (a letter, then up to 62 letters, digits or underscores), or was sent since, sort, cursor, limit or expand, which don't apply: stats answer over every record the filters admit. Send where and search only
open_field_too_fast429One person changed one record's open fields more than 10 times in a minute. Show the last state and let them try again in a moment
collection_not_declared400The collection has no declaration yet, and people other than the writer read it, so its fields are declared before anything is written. Add them to gemmein/collections/<name>.json: npx gemmein dev picks them up at once, and npx gemmein sync carries them to the cloud
undeclared_field400The write carries a field the collection's file doesn't declare; the message names it, with an example. Declare it under "fields" in gemmein/collections/<name>.json
field_deprecated400The write sets a field the collection's file marks "deprecated": true. A retired field takes no new values, and records that already have it keep it. Stop writing it, or remove the mark
posts_waiting409Owner approval can't be turned off, and the rule can't move to one without approval, while posts wait for it. The answer carries waiting, the count. Approve them or send them back (Data → the collection → Publishing), then make the change
confirm_required400On a collection's page in the dashboard's Data: a rule change that lets more people see or write the collection's existing records asks for the collection's name. Type it to confirm, then save
invalid_visibility400visibility isn't "followers", "circle:<id>" or "everyone", or it is set on a collection that isn't community or shared, or on a record with no person. Followers-only and circle posts belong in a community or shared collection
file_is_public400A followers-only or circle post names a file that is public, someone else's, or not uploaded for that audience. Upload it again with { visibility: "followers" } (or the circle's visibility) and put that ref in the post
person_unavailable403A follow, a message or a direct file across a block, whoever made the block. The answer never says who blocked. Don't retry, and tell the person they can't follow or message them
follow_limit429A person already follows the most people they can: 5,000 (200 in Development). Unfollow someone first
block_limit429A person already blocks the most people they can: 1,000 (50 in Development). Unblock someone first
request_limit429A person has the most follow requests waiting that they can have: 500 sent (50 in Development), or 1,000 received (100 in Development). Cancel one, or try again once some are answered
request_not_found404No follow request from that person is waiting. g.people.requests() lists the ones that are
invalid_private400private isn't true or false. true turns follows into requests the person accepts; false lets anyone follow and accepts the requests waiting
person_private403A private account's followers and following show only to the people it accepted. Follow the account (the person accepts) to see them
invalid_list400list on a people read isn't following, followers, friends, blocked, requests or requested (another person's lists are followers and following), or limit or cursor came without a list. Use the g.people methods
circle_not_found404No circle with that id has the person in it. g.circles.list() lists their circles
circle_creator_only403Only the person who created a circle adds or removes its members. Ask its creator
circle_creator_leaving400The person who created a circle is leaving it. Name a member to take it over (leave(id, { transferTo })) or disband it (disband(id))
circle_full429The circle holds the most members its plan allows: 50, 250 on Run a business, 1,000 on Grow. Remove someone first
circle_limit429A person already created the most circles they can: 100 (10 in Development). Disband one first
invalid_circle_name400A circle's name is 1 to 80 characters. Send a name in that range
not_in_circle403A circle's posts are for its members: a person posts to a circle only while they are in it. Ask the circle's creator to add them
circle_member_not_found404The dashboard, the CLI or your AI removed a person from a circle they aren't in, and nothing changed. people_graph lists a circle's members
circle_creator409The dashboard, the CLI or your AI removed a circle's creator from their own circle, and nothing changed. To stop them, suspend or delete the person
follow_not_found404The dashboard, the CLI or your AI ended a follow that doesn't exist: that person doesn't follow the other and has no follow request waiting, and nothing changed. people_graph lists who follows whom
invalid_query400A read of a person's follows names neither a person (person with a view of following, followers, requests, requested or circles) nor a circle (circle=cir_…), or env isn't development or live
rule_conflict409A create names a collection that already exists with another rule. A rule changes from the collection's card on the dashboard, not by creating it again; npx gemmein sync reports it as a conflict line
sensitive_field400A declared field's name says it holds health or medical data, biometric data, a government ID number or payment card details (ssn, passport_number, card_number, cvc, diagnosis and similar). Collections don't hold these, and payments go through Stripe. Rename the field if it holds something else. gemmein dev, npx gemmein sync, your AI's declare_collection and the dashboard's field editor all refuse it

Views

CodeStatusMeaning · what to do
unknown_collection400(Alongside this code's existing 404 row above — this status is specific to saving a view.) Saving a view names a collection this environment doesn't have. Declare the collection first (declare_collection, or npx gemmein sync from a gemmein/collections/*.json file), then save it again
add_not_enabled400Adding through a view: this view has no add declared. Push a view that declares one
edit_not_enabled400Editing through a view: this view has no edit declared. Push a view that declares one
field_not_in_form400Adding or editing through a view named a field the view's own add.fields or edit.fields doesn't list. Check the view's declared fields
invalid_data400Adding or editing through a view: the data sent wasn't a JSON object. Send a plain object of field values
invalid_view400A view's recipe doesn't match its collection: a column, total, layout or action names a field, or an action a choice, the collection hasn't declared — or, on an update, the definition tries to change the view's collection (fixed after creation; push a new view instead). The message names the field and the fix
views_capped400An environment holds at most 100 views. Remove one first
unknown_move400Pressing a view's button named a move that isn't one of that view's declared actions. Check the view's own action names
unknown_view404Deleting a view with dev_delete_view: Development doesn't have a view by that name. Check the name on the Views page
window_not_enabled400Adding or editing through a view sent showFrom or showUntil on a view with no window declared. Push a view that declares one
reorder_not_enabled400Reordering a view's rows: this view has no reorder declared. Push a view that declares one
move_not_allowed409A view's action button was pressed from a state the move doesn't allow — its from doesn't include the record's current value — or the field it writes is no longer a declared "one of" open field at all (narrowed or retired since the view was made: push the view again once its recipe matches)
open_value_not_in_list409Pressing a view's button re-checks the collection's CURRENT open-field choices every time, not the recipe's own cached ones: the move's to (or a listed from) is no longer one of them. Push the view again once its recipe matches (see also open_value_not_in_list at 400, the SDK's own open() refusal — the same name, raised elsewhere)
changed_since_opened409A record (or, on the dashboard, a collection's record editor) changed since the version you read: a view's action button was pressed, or a dashboard edit saved, against a version that has since moved. Re-read the record and reapply your change
reorder_not_allowed409Reordering a view's rows: the view's collection no longer declares a rankable field — it did when the view was pushed. Push the view again once its recipe matches
no_neighbor409Reordering a view's rows: moving a row up or down with no neighbour at that end of the view's own order. Nothing to move past
invalid_timezone400A tz parameter (reading a view's rows, or the dashboard's own record reads) isn't an IANA time zone name, such as Europe/Paris

Files

CodeStatusMeaning · what to do
unknown_file400A ref-shaped value points at a file that doesn't exist or isn't yours to hand. This is enforced on every rule, for creates and updates, from app code and the dashboard alike. Fix the ref; don't invent one
invalid_ref400The value passed to a file link call is not a file reference. A ref is file:<id>, the value upload() returned; pass that, never a filename or a URL
invalid_file_content400Uploaded bytes aren't the claimed type (usually a renamed file); send the real file, don't retry
unsupported_file_type415Upload isn't an allowed type: images (JPEG/PNG/WebP/GIF/HEIC), documents (PDF/ZIP/EPUB), audio (mp3/m4a/wav/webm/ogg/flac/aac) or video (mp4/webm/mov)
media_rate_limited429One person started 20 audio or video uploads this hour. Try again after resetAt
too_many_open_uploads429Too many uploads in progress: one person has 5 files of any type started and not confirmed, or 1 GB of them. Confirm one, or wait an hour
person_storage_full429One person's files would pass their share: the larger of 10% of the app's storage or 2 GB
usage_limit_exceeded429Storage is past its limit and the grace (the lesser of 10% over or 7 days) has run out; uploads refuse until the owner deletes files, adds a storage pack or moves the app up a tier. Files already stored stay. No resetAt: the limit is on stored bytes, not time. (Counter usage is never refused or charged: past a monthly ceiling the owner is told once a month)
invalid_file400The file is empty
invalid_upload400The upload call is missing its key
upload_incomplete400Confirm was called before the bytes finished uploading; finish the upload first
already_confirmed409This upload was already confirmed; don't confirm twice
too_large413On npx gemmein dev: the upload is over 510 MB, the most it stores. The SDK reports it as upload_failed with status 413; send a smaller file
upload_refused403On npx gemmein dev: the bytes don't match the upload that was started (another file name, type or size, or the upload expired). The SDK reports it as upload_failed with status 403; start the upload again
file_quarantined403The stored bytes failed the content check; the file is held and won't be served

Messages and email

CodeStatusMeaning · what to do
thread_not_found404An AI reply named a conversation that doesn't exist, belongs to another app, or that no customer started (a send the customer never answered). An AI replies only inside a conversation a customer started; list them with GET /server/inbox/threads or the connector's live_inbox
person_erased409The conversation's customer was erased, so their address is gone from it. A reply, from an AI or the dashboard's Inbox, is refused with nothing sent: "This person was erased — there is no address to reply to." Fetching one of their messages again is refused too: "This person was erased — their message is not fetched again."
inbox_unreadable409The customer's last message is still arriving, or Gemmein couldn't read it, so an AI can't tell whether a person wrote it. Nothing was sent. Try again in a minute; reply yourself from the dashboard's Inbox meanwhile
inbox_unauthenticated409The customer's last message didn't pass DMARC, so its sender isn't proven, and an AI reply could reach someone who never wrote. An AI can ask to reply only to senders whose email passes DMARC; nothing was sent. Reply yourself from the dashboard's Inbox, where the message is marked as an unverified sender
inbox_auto_reply409The customer's last message wasn't written by a person (an auto-reply, an out-of-office, a bounce or a mailing list), so an AI reply could start a mail loop. Nothing was sent. Wait for the customer to write, or reply yourself from the Inbox
inbox_reply_cap4093 AI replies in a row with no answer from the customer. Nothing was sent; the next AI reply waits for the customer to write. Reply yourself from the Inbox
inbox_thread_limited429This conversation has had its 10 AI replies for today. Nothing was sent; resetAt says when the next can go. Reply yourself from the Inbox
inbox_recipient_limited429This address has had its 5 AI replies for this hour, across every conversation. Nothing was sent; resetAt says when the next can go
inbox_reply_limited429In Live, AI replies passed 60 an hour for this connection or key, or 300 an hour for the app across every AI sender. Nothing was sent; resetAt (and Retry-After on the key route) says when the next can go. Reply yourself from the Inbox
html_not_allowed400Community/addressed/direct store plain text; remove HTML tags, or you turn on the collection's allowsMarkup setting (Content: allows markup) when the app renders HTML safely
missing_text400A send with no text; send something
text_too_long400The text exceeds the maximum length; shorten it
invalid_recipient400A support reply refused: the recipient is not a single plain address
invalid_audience400for isn't a person in this app; fix the recipient id
too_many_recipients400A direct message, or a file uploaded to a direct collection, names more than 20 people; send one addressed record each, or use a shared collection
invalid_recipients400A list of recipients sent to a collection that is not direct (a record or a file upload); addressed takes one person per record
reply_only403This direct collection only allows replying to people who wrote to you first
sends_disabled403The owner turned direct messages off for this collection; addressed records are sent from the dashboard or a relay
not_a_customer404notify()'s recipient isn't a verified person of this app and environment. One code covers both cases on purpose: existence is never leaked
notify_capped429notify() hit a cap of 5/person/day (event class) or 200/app/hour. Wait for resetAt; account-class skips only the per-person cap
no_person400notify() was sent without a personId. Pass the person's user id in this app, never an email address
no_subject400notify()'s subject is missing or over 300 characters; the message says which
no_text400notify()'s text is missing or over 10,000 characters; the message says which. It is plain text: a notification, not a campaign
invalid_kind400notify()'s kind is neither "event" (capped per person) nor "account" (account activity: a sign-in, an access change, a billing problem)
send_failed502The mail could not be sent, or the sending domain could not be checked just then. Nothing was sent or recorded and no cap is spent: retry notify() with the same key. In the dashboard Inbox, try again in a minute
content_slot400An inbox-reply template needs {{content}} exactly once
development_capped429Cloud Development's email budget is spent: notify(), a relay's email_person and the purchase email share 20 a day per app and 50 a day per account. Wait for resetAt, or test volume locally with npx gemmein dev. Live has no such budget
reserved_key400A notify() key starting gemmein:. Those keys are Gemmein's own; choose another
in_flight409A notify() with the same key is being sent right now. Retry in a moment; a delivered send answers idempotently
invalid_brand400sign-in.json is not a valid brand config: an unknown key, a greeting or app name with a digit or an address, a colour that is not #rrggbb or is too light to read the code on, or a logo that is not an https: image. The message names which
logo_unavailable400The logo in sign-in.json could not be fetched when you saved: it is not a PNG, JPEG, GIF or WebP, is over 200 KB, redirects, or answered an error. Nothing is saved; fix the address or the image and save again
template_too_large400An email template is over 100 KB. Host images at https: addresses and trim the HTML
templates_capped429Email templates take 60 saves and 120 previews an hour per app (150 and 300 per account). Wait for resetAt, or preview locally with npx gemmein emails preview
templates_live_refused409Email templates are saved in Development; go-live and promote copy them into Live
unknown_template400An email template's name is not one Gemmein sends: sign-in, purchase, notify, notify-account, notify-event, email-person or inbox-reply
unknown_variable400An email template uses a {{var}} its email does not have. The message names the ones it has
sender_domain_required409notify(), a relay's email_person, and the dashboard Inbox (a new conversation or a reply) send only from your own verified sender domain. Until one is verified on the Domains page, the send is refused: nothing is sent or recorded, no cap is spent, and the same key sends once you verify. Sign-in codes are the one email Gemmein sends on your behalf before that, as <App name> (via Gemmein). A test member’s email in Development needs no domain: it lands in the test mailbox
already_waiting409The dashboard Inbox: a new conversation to someone who is already waiting on a reply under the same subject. Reply in that conversation instead
start_rate_limited429The dashboard Inbox: 30 new conversations in an hour from this app. Wait a moment; replies in existing conversations are not counted
not_a_member403The dashboard's alert settings: the email signed in to the dashboard isn't this account's. Sign in with the account's email and try again
inbound_cap429Mail to your support address past the Inbox ceiling: one conversation holds at most 500 messages, and an app at most 2,000 conversations. Mail past it is not added to the Inbox; a customer with a full conversation can write under a new subject

Payments and access

CodeStatusMeaning · what to do
entitlement_required403Signed in, but without the plan or product that unlocks this. On a collection: not on a plan (or holding a product) the collection is unlocked by, and err.requires carries that plan's key (one string). On an AI tool: the tool's requires names a plan or product this person lacks, and the message names it. Show an upgrade path, and retry after they have it
payments_not_configured404No plans or products exist in this environment, or Stripe isn't connected in this environment. Don't retry; show a message
unknown_product404No product by that name in this environment. The message is safe to show the buyer; the owner's Logs list what this environment sells, and in Development the response carries it as ownerDetail
unknown_product400A g.payments.buy() that names no product. The message lists what the app sells; name one
unknown_plan404Checkout for a plan that doesn't exist. The message lists the app's plans, so there is nothing to check out
unknown_plan400From grantAccess(): no plan or product by that name. The owner adds it on the Payments page
plan_has_no_link409The plan has no Payment Link Gemmein made in this environment yet. Show the message; the owner connects Stripe and prices the plan on the Payments page in the dashboard
already_subscribed409The signed-in person already has a subscription, so no second one is started. Send them to the url in the answer (a Stripe customer portal session Gemmein opens for them), where they change plan
no_subscription409g.subscriptions.manage(): the signed-in person has never paid for a plan through Stripe, so there is nothing to manage. Show the plans instead
no_subscription400Your own Gemmein billing, on Usage & billing: adding or removing a storage pack needs an active Gemmein subscription. Finish billing setup, then try again
invalid_plan400Your own Gemmein billing, on Usage & billing: a plan change named an unknown tier or interval. A tier is Launch, Run a business or Grow; billing is monthly or yearly; one change moves either the account’s tier or the billing interval, not both
interval_locked409Your own Gemmein billing: your year is paid, so monthly billing opens 30 days before it renews; the message names the date. Try again from then
not_live409Your own Gemmein billing: changing the tier of an app that isn’t live. Its tier is chosen when it goes live
storage_over_new_limit409Your own Gemmein billing: removing a storage pack or moving a Live app down a tier while its files don't fit the storage it would have afterwards (its tier's storage plus the packs left). Nothing is charged or changed. Delete files, or keep or add a pack, then try again
no_plans409g.subscriptions.manage(): the app sells no plans (none listed, and no subscription still charging). A plan removed while members still pay keeps their portal open
portal_unavailable409g.subscriptions.manage(): the customer portal can't open right now, because Stripe isn't connected here or the key lacks Customer portal: Write. The message is safe to show; in Development, ownerDetail says what the owner fixes on the Payments page
portal_unavailable503g.subscriptions.manage(): Gemmein couldn't read the app's payments for a moment. Nothing changed; let them try again
portal_unavailable502g.subscriptions.manage(): Gemmein couldn't reach Stripe to open the portal. Stripe was unreachable; nothing in your app needs fixing. Nothing changed; let them try again shortly
portal_rate_limited429g.subscriptions.manage(): the person opened the portal more than 10 times in 10 minutes. Development also allows an app 100 opens an hour. Show the message and let them try again in a few minutes
checkout_in_progress409The same person already has a checkout open for this plan. Show the message; they finish it there, or try again in a few seconds
product_not_sellable409g.payments.buy on a product sold through a relay, not sold yet, or with no Payment Link Gemmein made, so it has no Payment Link to open
plan_not_sellable409g.subscriptions.checkout on a plan sold through a relay, or not sold yet, so it has no Payment Link to open
plan_not_purchasable400That's the free default plan; nothing to buy
invalid_item400The item on a purchase breaks the allowed shape
simulation_failed502Development only: the local payment simulator couldn't complete the round trip
payments_off409Payments are turned off in this environment, so checkout and buy are refused and nothing can be bought here. To sell here again, set payments up again on the dashboard's Payments page
invalid_email_for_checkout400The signed-in person's email address is too long to carry through Stripe checkout, so no checkout was made. Ask them to sign in with a shorter address

Secret key calls

CodeStatusMeaning · what to do
session_invalid401verifySession(): no session matches this token; send the person to sign in again. Never store or reuse a token across people
session_expired401verifySession(): the session ended (the message names when); send the person back to sign-in
session_revoked401verifySession(): a newer sign-in, a sign-out, or the owner ended this session; send the person back to sign-in
person_suspended403The owner suspended this person; access stays off until the owner reactivates them in the dashboard. holdings() still answers, flagged suspended: true
person_not_found404No person with this id in this app and environment; ids come from verifySession() or the dashboard, never from an email. Existence is never leaked
capability_required403The secret key doesn't have this permission; the owner creates a key with "Look up a person's access by id", "Grant and revoke access", "Create a person by email before they sign in" or "Spend a person's credits" ticked. Purchases still come only from Stripe
already_revoked409A grant ends once, and revokedAt is set and never edited. It is already ended; don't retry
grant_not_found404Not this person's grant, in this app and environment; re-read holdings() for the current grant ids
invalid_source400A purchase or subscription was asked for by hand, and that is refused. Access that money made comes only from Stripe or from the relay a product or plan is sold through; a key grants manual, trial, promotion or migration
invalid_entitlement400Not a valid access:<slug> key; fix the slug, or drop the key entirely and pass the plan's or product's own name, which grantAccess() resolves for you
invalid_id400A prototype name (__proto__, constructor) was sent where a person id or a grant id belongs. Ids come from verifySession() or the dashboard, never from a name you built
unknown_route404This path is not a /server route; the message lists the valid ones. Call the server client's methods instead of building the path by hand
method_not_allowed405The right route, the wrong verb: verifySession(), grantAccess(), revokeAccess(), invitePerson(), notify() and spendCredits() are POST, holdings() is GET
invite_capped429invitePerson(): 500 invite calls per app per day, a fetch of an existing person counting too; a receiver's person_email shares the cap and counts only a person it creates. The limit is temporary; write to hello@gemmein.com to raise it. resetAt says when the window ends
person_required400A secret key's create() or upload() on private, shared or community names no person; pass { for: personId }. On direct, name both: { from: authorId, for: recipientId }
invalid_person400A secret key named a person where it can't: { from } on anything but direct, any person on admin_write or public_read (those records are the app's), or an id that isn't a person of this app (ids come from verifySession(), invitePerson() or the dashboard). From app code, { from } is refused: the signed-in person is always the author

Relays

CodeStatusMeaning · what to do
scheme_renamed401A relay saved before the hmac_sha256_header scheme was renamed signature refuses every event until its definition is saved again with "scheme": "signature" (the relay's page, or npx gemmein sync); nothing else changes. A definition that names the old scheme is refused at save with invalid_definition, naming the new one
too_many_signatures400The signature header lists more than 8 signatures. A sender lists one, or two or three while it rotates its secret. Check that signatureKey and separator match the sender's docs
unsigned_event409Replay was refused: the event arrived without its content signed (the sender signed only a token, or sent a shared token), and the relay now runs an action that needs trigger.trustUnsignedEvents. Nothing ran. Set the opt-in on the relay to let such events act, or let this one go. See Unsigned events
no_call_url400A new signing secret was asked for on a relay that calls no URL, so nothing it does is signed. Add a call_url action to its file first
provider_secret_required400Gemmein was asked to make the secret for a relay whose sender always makes its own: Stripe's format (paste its whsec_…), or an ed25519 or ecdsa-p256-sha256 recipe (paste the sender's public key). Paste it on the relay's page (Relays), or pipe it to npx gemmein relays secret <name>
unknown_receiver404A relay receiver: no receiver with this name on this app. A deleted relay no longer receives; a paused one still does
bad_signature401Receiver: the signature did not verify against this receiver's secret. Check the secret the provider signs with and the header the scheme expects
body_too_large413Receiver: the body is over 256 KB
invalid_json400Receiver: the body is not JSON
receiver_capped429Receiver: more than 120 verified events in a minute for this app; resetAt says when. Unverified traffic never spends this budget
source_capped429Receiver: more than 60 unverified calls in a minute from one address. Verified events count against the app, unverified ones against the address they came from; resetAt says when
not_recorded503Receiver: the event could not be stored, so nothing ran. The provider sends it again
invalid_definition400A relay file has a problem; the message is one sentence naming the field, why, and what to do
duplicate_grant_target400A relay has two grant or take actions on the same target: a product, an access key, a plan, or credits. A relay grants or takes each target once; merge the two into one action (for credits, one grant_credits with the summed amount). The message names the action and its target
relay_capped400The environment holds 150 relays. Delete one, or fold two into one; limits are raised on request
name_taken409A relay with this name exists in this environment
version_conflict409The relay's definition moved since it was read; re-read and reapply
not_saved503Updating a relay that gains an action moving money or access: its waiting events couldn't be prepared for the change, so nothing was saved. Try again
not_replayable409Replay applies to a dead, failed or done event; a queued or running one is already on its way
trigger_kind_fixed400A relay's trigger kind can't change once it exists: its receiver address, secret, schedule or hook would not carry over. Give the relay with the new trigger a new name in gemmein/relays/, or delete this one and create it again; the message says which applies
relay_missing400A relay action, or a product's or plan's "How it's sold", names a relay that doesn't exist in this environment
not_callable404g.relays.call: no relay of that name has an app_call trigger. Check the name, and that the relay's trigger is { "kind": "app_call" }
relay_paused503g.relays.call: the relay is paused, so nothing was charged. Resume it on the Relays page, or try again later
relay_failed502g.relays.call: what the charge paid for failed (the collection was removed, the write was refused). The credits came back once; the answer carries refunded: { credits, balance }, or refundPending while Gemmein retries the refund until it lands. It is also the alert a relay raises when an event ends dead
call_in_progress202g.relays.call: this key's call is still working. Ask again with the same key, never a new one. The answer says when to ask
call_not_finished409g.relays.call: this key's call ended without charging, or was refunded. Send a new key to try again
too_many_calls429g.relays.call: one person, or the app, passed the calls it may make in a minute; resetAt says when. A person gets 30 a minute per relay
daily_cap429g.relays.call: the app has taken all the calls it can today, or all it takes from people who are not signed in. Try again tomorrow, or sign the person in
internal_error500g.relays.call: the call could not be recorded, so nothing ran and nothing was charged. Try again with the same key

Functions

CodeStatusMeaning · what to do
functions_unavailable501Functions isn't switched on for your account. put_function, running a function, and a relay's run_function step all answer this the same way until it is. The npx gemmein dev local runtime answers the same code for a different reason: it runs no Edge functions at all — push with npx gemmein functions push <name> and run them in your Development (the cloud one), with the same limits
edge_function_lock_busy503Another deploy, save or remove of this same function is already running — this one answered busy rather than wait indefinitely. Nothing about the function changed. Run the same command again in a moment; Edge serializes one function's deploys so this should clear within seconds once the one ahead of it finishes
secret_hosts_in_browser403A function's Live secret is bound to the host(s) your human ticks in their own browser, in the approval Gemmein opens — never typed or chosen by the CLI or an AI directly. Nothing was stored by the call that got this refusal; stage the value (piped from stdin, never on the command line) and open that approval with npx gemmein functions secret <function> <SECRET> --host <host>
remove_failed502Deleting a function timed out partway through, so it may not have been removed from Cloudflare. Nothing about the stored listing changed otherwise. Run npx gemmein functions delete <function> again to confirm it either way

AI tools and credits

CodeStatusMeaning · what to do
unknown_tool404No AI tool by that name in this environment. See AI tools
tool_disabled403The owner switched this AI tool off
tool_incomplete409On g.ai.run: the tool composes nothing, no prompt template and no inputs
invalid_inputs400On g.ai.run: an input is unknown, missing, the wrong type or over its cap; the message names it
raw_calls_off403The browser may not compose provider requests for this provider (g.ai.chat). Call a named tool with g.ai.run, or switch raw calls on for the key on the AI page
model_pinned403This AI tool's model is fixed; leave model out of the body
model_not_allowed403The owner's allowlist does not name this model; the message lists the ones it does. Only applies to a tool with no pinned model
provider_required400More than one provider key is set and the call named none; pass provider
ai_not_configured409No provider key on this app and environment; the owner pastes one on the AI page. message is for the person ("This feature isn't available right now."); in Development the owner's sentence is in ownerDetail, and Live never carries it. See AI tools
invalid_base_url400An OpenAI-compatible provider's base URL was refused when it was saved: it is http://, an IP address, a gemmein.com host, or carries a user and password, a ?query or a #fragment. The message names the rule. Give the https:// base URL the provider's docs show
endpoint_refused502An OpenAI-compatible endpoint's base URL resolved to a private, loopback, link-local or cloud-metadata address when the call ran, or the endpoint answered a redirect, which is never followed. Nothing was spent and no connection was made. message is for the person ("This feature isn't available right now."); the AI page's last error names the rule. Point the base URL at the final, public address
endpoint_budget429An OpenAI-compatible endpoint's answers passed this environment's budget: 50 MB in an hour or 500 MB in a day in Development, 1 GB and 4 GB in Live. Refused before anything is spent, until the window opens again. The owner's sentence names when; the person reads a neutral one outside Development. Lower the tools' output caps; in Live, write to hello@gemmein.com to raise it
endpoint_busy4295 calls to the endpoint were already open at once in Development (50 in Live). Nothing was spent: "Too many AI answers are running at once for this app — try again in a moment." Retry after a moment
environment_lookup_failed503Gemmein couldn't confirm this app's environment just now. Nothing was spent; try again in a moment
session_required401g.ai.chat() or g.credits.balance() without a signed-in person; sign in first
ai_capped42920 AI calls per person per minute; wait for resetAt
provider_unreachable502The provider did not answer before the first byte; nothing was charged and the credit is refunded. Retry
provider_errorthe provider'sThrown by g.ai.text() and g.ai.runText(): the provider's own non-2xx, its message in err.message; g.ai.chat() and g.ai.run() return it as it came. See AI tools
too_many_tools409An environment holds at most 150 AI tools
invalid_tool400Creating or updating an AI tool with a bad field; the message names it
unsupported_response_format400A transcribe tool asks its provider for a format Gemmein can't store: text, srt, vtt, a template, or verbose_json on a model that doesn't offer it. Refused when the tool is saved, never during a run; the message names the formats that work
ai_test_capped429The AI page's test call; one a minute per app
credits_exhausted402The person's balance is below the spend; the message carries the balance ("this person has {balance} credits — the spend needs {amount}"). Show the pack; never retry the same spend. See Credits
invalid_amount400spendCredits(): amount outside 1 to 10,000, or not a whole number
invalid_reason400spendCredits(): reason missing, not text, or over 200 characters
dedupe_conflict409spendCredits(): the key already names a different movement, another kind or another person; a key is one movement, reused only to retry that same one
credits_ceiling409A credit that would carry the balance past 1,000,000,000; nothing was added. See Credits
audio_not_accepted400g.ai.upload named a tool that isn't a transcribe tool. Audio uploads go only to a transcribe tool; upload images and documents to one of your collections and pass the ref
unsupported_audio_type400g.ai.upload: the file isn't audio this tool takes, or has no type. Send mp3, m4a, wav, webm, ogg or flac with its audio content type
input_not_accepted400On g.ai.run or g.runs.start: a file input is one the tool doesn't take as a file, or a ref that isn't a file it accepts; the message names the input
input_not_supported409The tool's model doesn't take this kind of file (image, audio, video or document). The owner picks a model that does, or unticks that kind on the tool
input_too_large413A file input is over the size this tool takes for its kind; the message names both sizes
input_busy429One large file at a time per person, or too many files are being handed to providers right now. Try again when the last one is handed over
input_rejected502The provider didn't accept the file, or the request to it couldn't be put together; the message carries the reason
runs_capped429g.runs.start: the person already has 10 runs open (queued or running). Wait for one to end, or cancel one with g.runs.cancel
unknown_run404g.runs.get, watch or cancel: no run by this id for the signed-in person
run_ended409g.runs.cancel on a run that already ended, or your pipeline called /complete, /fail or /progress on one. Read its status
run_busy409Your pipeline: a completion of this run is already in progress. Wait for its answer, up to five minutes if that call was lost
not_external409Your pipeline called /complete, /fail or /progress on a run a provider executes; such a run completes on its own
token_required401Your pipeline called /complete, /fail or /progress without Authorization: Bearer <token>. The token came with the hand-off
token_invalid401Your pipeline's token doesn't match this run, or no run has this id (the same answer either way)
hook_capped429Your pipeline: more than 120 calls a minute to the run's /complete, /fail or /progress from the same sender. Wait a minute and send again
result_refused422Your pipeline's /complete couldn't be stored, or its tool no longer exists. The run failed and its hold was returned
ai_key_rejected400The provider rejected the key in its one test call, so the key isn't saved or copied (on the AI page, a key page, or when Live takes Development's key). Paste a key that works
ai_key_unreachable400The provider couldn't be reached for the key's test call, so the key isn't saved or copied. Try again in a moment
ai_verify_capped429More than 6 keys were tried in a minute for this app. Try again after resetAt
reserved_name400An OpenAI-compatible endpoint's name includes a provider Gemmein connects directly. Choose that provider from the list, or give the endpoint a name of its own (like OpenRouter or Azure)

Sync and going live

The codes npx gemmein sync, npx gemmein go-live and the MCP server can receive. Each message names the fix.

CodeStatusMeaning · what to do
use_promotion400A community, shared or direct collection was created straight into Live. Create it in Development, then go live or promote; it reaches Live that way, with its shape confirmed
invalid_unlocked_by400A collection's unlockedBy isn't a list of plan or product names: it names an access key instead of a name, names more than the limit, or the app has no plans or products yet to unlock with. The message names the fix
if_name_required400Renaming the app needs ifName, the name you last saw, so a rename never overwrites one it didn't see
name_changed409The app was renamed since you read its name; nothing was renamed. The body carries current; read it and try again
invalid_declaration400gemmein/payments.json can't be read; the message names the problem. Fix the file and run npx gemmein sync again
declaration_ambiguous409gemmein/payments.json could match more than one plan or product already in Stripe; the message says which. Give the item its own name or key
file_not_uploaded400A product in gemmein/payments.json delivers a file this sync hasn't uploaded yet. Run npx gemmein sync, which uploads it first
payments_live_refused409Payments are set in Development only: npx gemmein sync never changes Live's payments. Going live and promoting copy Development's payments into Live
live_paused402Going live or a promote while Live is paused (npx gemmein go-live, Promote on the Go live page). The message is the founder's sentence; npx gemmein go-live says it before asking for any approval, and the preview of what would go still answers. Pay first from Settings → Usage & billing, then go live
undeclared_collections—Going live was refused before the card step, and nothing was charged: a community, shared or direct collection has no fields declared. The message names each one. Declare its fields in gemmein/collections/<name>.json and run npx gemmein sync
unknown_collection_setting—A collection file carries a key the grammar doesn't know, refused by name at gemmein dev and before sync calls the cloud: "feed.json: "whoCanStart" isn't a collection setting — the settings are rule, means, unlockedBy, publishing, openFields, directInitiation, fields". Nothing is created or changed
invalid_fields—A collection file's "fields" is refused: not an object, a field type Gemmein doesn't know, or a field shape it can't read. The message starts with the file name and names the field. Nothing is created or changed
invalid_direct_initiation—A collection file's "directInitiation" is refused: it is on a collection that isn't direct (who starts a conversation means nothing there; remove it), or its value isn't one Gemmein knows
payments_live_refused403The Live push key (sync --live) asked to change payments. Live's payments come from Development by going live and promoting
not_available404On npx gemmein dev, gemmein/payments.json is read from your files directly; npx gemmein sync sets the cloud's Development
not_available501Exporting a customer's data isn't available here. Nothing was exported
app_session_cannot_manage403A person signed in to the app called a route that only the CLI's management sign-in reaches. An app sign-in is a customer, the owner's own included; the CLI manages after npx gemmein login, approved in the browser, and everything else is in the dashboard
insufficient_permissions403The key wasn't approved by the account's owner. Run npx gemmein login and approve it in your browser
invalid_input400A field the CLI sent is missing or not one Gemmein accepts, for example a collection name, a domain, a relay's ifVersion or a secret scan's result. The message names the field
missing_name400Deleting a collection without naming it. Name the collection
invalid_alias400A model alias in gemmein/ai has a name, provider or model Gemmein doesn't accept; the message names the fix
alias_in_use409A model alias that AI tools run on can't move to another provider, or to a model a tool can't use; the message names the tools. Point them elsewhere first
too_many_aliases409An environment holds at most 50 model aliases. Remove one first
version_required428Applying a payments sync without the version the plan read. Update the gemmein CLI and run npx gemmein sync again
invalid_plans400gemmein/payments.json, or Live's plans when going live or promoting, would be refused; the message names the plan and the fix
invalid_products400gemmein/payments.json, or Live's products when going live or promoting, would be refused; the message names the product and the fix
name_conflict409A plan and a product share a name. Rename one of them
invalid_consent400The 14-day waiver switch isn't { "digitalWaiver": true, "termsUrl": "https://…" } or { "digitalWaiver": false }, or termsUrl isn't an https address
consent_terms_url_required400Turning on the 14-day waiver needs termsUrl, the link to your Terms of Service. Nothing was changed
payments_closed409Payments are turned off in this environment, so nothing can be added or copied into it. To sell again, delete payments on the Payments page and set them up anew
stripe_key_required409npx gemmein sync: a plan or product sells through Stripe and Stripe isn't connected here. Connect Stripe with a restricted key on the Payments page, then sync again
price_required400npx gemmein sync: a plan or product sells through Stripe without a price; the message names it. Gemmein makes its Payment Link from the price, so give it one
invalid_receipts_collection400npx gemmein sync: the receipts collection on the Payments page no longer exists here, or isn't addressed (each buyer sees only their own). Pick an addressed collection for receipts, then sync again
invalid_library_path400The Library path starts with / and names a page on your own domain, like /library (at most 200 characters)
invalid_return_path400The return path starts with / and names a page on your own domain, like /thanks (at most 200 characters)
invalid_mappings400npx gemmein sync: a “When Stripe says” rule on the Payments page no longer holds, for example its collection was deleted; the message names the problem. Fix or remove the rule, then sync again
stripe_account_in_use409npx gemmein sync changing payments: this environment's Stripe account is connected to another Gemmein account, so payments here can't change. Contact support to move it. Connecting that account's key on the Payments page is refused the same way
stripe_key_needed409A plan or product priced to sell through Stripe needs a Stripe key here; the message names the item. Connect Stripe with a restricted key on the Payments page, then sync again
stripe_key_broken409Stripe no longer accepts the key Gemmein holds (revoked or changed), so Gemmein stopped using it. Connect a new key, then try again. Payment Links already out keep selling
stripe_key_too_broad409The Stripe key was changed and can now do more than Gemmein needs, so Gemmein stopped using it. Fix it in Stripe, then try again
stripe_key_rejected409Stripe refused the key while Gemmein worked in Stripe; nothing was saved. Connect Stripe again with a new restricted key
stripe_key_permissions409The Stripe key lacks a permission this change needs; the message names it. Add it to the key in Stripe, then try again
stripe_key_plan_rows409A first plan sold through Stripe (by a sync, on the Payments page, or by a promote into Live) on a key made with the five permissions for products. A key that sells plans needs two more: Customer portal and Subscriptions, both under Billing, set to Write. Add them to the key in Stripe and save again; nothing was changed
payments_unreadable503Connecting a Stripe key on the Payments page: Gemmein couldn't read this environment's payments to check the key. Nothing was saved; try again in a moment
stripe_unreachable502Gemmein couldn't reach Stripe. Nothing was saved; try again in a moment
stripe_terms_url_missing409Stripe needs your Terms of Service link before a checkout can ask buyers to agree to it. Add it in Stripe (Settings → Business → Public details), then try again. Nothing was changed
stripe_link_off409A Payment Link Gemmein turned off can't sell again; the message names the item. Set a price to make a new one
payments_not_saved503Gemmein couldn't save payments just now. Nothing was changed; try again in a moment
payments_unconfirmed503Gemmein couldn't confirm the payments save. Read payments again (npx gemmein payments --cloud) to see what was saved
bad_responsethe status answeredA payments sync got an answer Gemmein couldn't read; the message carries the start of it. Try again
no_template404gemmein emails: that email has no template in this environment, so it sends in Gemmein's plain design. Add gemmein/emails/<name>.html and sync
template_empty400An email template with no HTML. Put the template's whole source in the file
provider_unavailable502Removing a domain: the mail provider couldn't take its email half down. Nothing was removed; try again in a minute
already_live400npx gemmein go-live on an app that is already live. To send later changes to Live, run it again to promote
not_live400Promoting before the app has gone live. Go live first
payment_required402Going live needs a card on Gemmein billing. Add a payment method and finish billing setup, then go live; charging starts when you go live
billing_past_due402Going live or buying a plan: your last payment for Gemmein didn't go through. Update your card in Stripe (Settings → Usage & billing → Open the billing portal), then go live. No payment page is made until then
billing_paused402Going live or buying a plan: your Gemmein subscription is paused. Resume it in the billing portal, then try again
billing_unavailable503Going live or buying a plan couldn't check billing with Stripe. Nothing changed; try again in a moment
go_live_blocked400A collection your users write to has no confirmed shape yet; the message names it. Confirm its shape, then go live
go_live_busy503Going live or buying a plan: Gemmein is still finishing your last try. Nothing new was charged. Try again in a minute
go_live_unfinished503Going live didn't finish. The message says which of four things happened: the charge was credited; nothing was charged; it was paid, with one step left; or nothing is live yet and nothing was charged, so press Go live again in a minute. Then press Go live again (from an AI app: ask for a new approval)
plan_changed409Going live: the plan or its price changed since it was shown. Nothing was charged. Look at the plan again, then go live
plan_unconfirmed503Going live: Gemmein couldn't confirm your plan change with Stripe. Nothing is live yet. Press Go live again in a minute
plan_unreadable409Going live or buying a plan: Gemmein couldn't read your plan. Try again; if it keeps happening, contact support
invalid_interval400Going live: the billing interval must be month or year. Pick one and try again
invalid_field_decision400Promoting: a decision for a new field is missing or isn't one Gemmein knows; the message names the field
shape_changed409Promoting: a collection's shape changed in Development since the list was shown. Run promote again to see the current shape
offer_changed409Promoting: a field changed in Development since the list was shown; the message shows it then and now. Run promote again to see the current shape
gate_target_missing409Promoting: a collection or AI tool in Live is unlocked by a plan or product Live doesn't have and this promote won't add; the message names each one. Pick who unlocks it on Live's Data or AI page, then promote again
payments_changed409Promoting: Live's payments changed since you looked. Load them again and promote again
review_required428Promoting without naming the Development you reviewed. Update the gemmein CLI, then promote again
development_changed409Promoting: Development changed since you reviewed it. Review again, then promote
changed_since_review409Promoting: an item changed in Development since you reviewed it; the message names it. Review again, then promote
development_unreadable503Going live or promoting: Gemmein couldn't read your Development just now. Nothing was changed; try again
no_paid_plan409Promoting would leave Live with no paid plan; the message names the plan. Pick Live's paid plan on Live's Payments page first
not_live409The app isn't live yet, so Live has nothing to promote or sync to and no Live keys to make. npx gemmein go-live takes it live first; until then, make a Development key
field_missing_from_declaration400Promoting: a field Live has is missing from the collection's file; the message names it. Put it back, or mark it "deprecated": true to retire it. A field is never retired by leaving it out
field_type_change400Promoting: a live field changed kind in Development. A live field never changes kind: add a new field with the new kind, and mark the old one "deprecated": true
tighten_needs_decision400Promoting: a field becomes shorter or required, and the message counts the records it affects. Existing records are never cut or hidden. To apply it to new writes and edits only, send tighten: { "<collection>.<field>": "new-writes-only" }; the dashboard's Promote asks for you
invalid_relay400A change to Live you asked to approve names a relay Gemmein would refuse. Fix its file in gemmein/relays/ (npx gemmein sync names what is wrong), then ask again
too_many_changes400One approval covers at most 100 changes. Split the change into smaller ones
beyond400Your AI's buy_plan, npx gemmein plan buy or Pay now asked for the Beyond tier (above 25 apps), which has no checkout. Talk to Gemmein at hello@gemmein.com; nothing was set up
plan_ended402Buying a plan: the account's Gemmein plan has ended, so its Live apps are paused. Restart the plan in Settings → Usage & billing, then buy this app's plan
not_on_cloud503Your AI's buy_plan or npx gemmein plan buy on the local runtime (npx gemmein dev). A plan is bought on Gemmein's cloud
checking_with_stripe503The plan payment page is asking Stripe whether a payment went through before it closes. Nothing is said about money until Stripe answers. Reload the page in a minute
payment_confirming409Stripe has a payment for this app and Gemmein is confirming it. Reload the page in a moment. Nothing is charged twice
plan_mode_changed409The plan payment page changed while it was open. Reload the page
plan_unfinished503The plan payment didn't finish. The page says whether the charge was credited or nothing was charged. Try again in a few minutes

Sites

The codes publishing a site can receive: the site tools in a chat (site_draft, site_put_file, site_publish and the rest), npx gemmein publish and npx gemmein site. A publish that fails answers FAILED with its step, the file when known, the reason and the fix; nothing changes at the site's address. See Publish your site.

CodeStatusMeaning · what to do
sites_unavailable503Publishing sites is not switched on where this request was answered. Nothing was published; try again later
development_only403A site is published from Development, with the Development key. Publish there; the Live site changes only when you approve putting a Development version live
secret_file—A chat put named a file that is never sent to a site: a secret-shaped file (.env, *.pem, *.key, id_rsa and the like) or a dot-file. Nothing was put. A key belongs in a relay or the app's own server; the page uses its public key
invalid_path400A path was refused. A site file’s path starts without a slash, has no empty, . or .. parts, uses letters, digits and . _ - ~ (also ! $ & ' ( ) * + , ; = : @), is at most 300 characters and is not under __gemmein/; the message names the rule it broke. A request’s own path with an empty, . or .. segment, a backslash or an encoded separator is refused too: call each route by its own path
draft_not_found404No such draft for this app. A draft not published within 7 days is discarded. Start a new one (site_draft) and put the files again
file_too_large_for_chat413A file put through a chat carries at most 1 MB (a base64 file about 750 KB). Publish larger files from a computer with npx gemmein publish
too_many_files413A site holds at most 2,000 files. Remove files the site does not need, or serve them from storage made for them
site_too_large413The draft would be over 50 MiB, the most a site holds. Remove or shrink files, or serve large ones from storage made for them. On a download: that version is too large to hand back as one zip
invalid_manifest400npx gemmein publish sent a file list Gemmein can't read: each entry is a path, a hash, a size and a content type, and the message names what is wrong. Update the CLI and publish again
publish_in_progress409Another publish of this site is running. Try again in a few seconds
publish_not_found404No such publish for this app, or one that was not finished within an hour and was discarded. Run the publish again
publish_done409That publish was already finished. Publish again for a new version
site_file_mismatch409A file Gemmein holds, or the copy it received, does not match its hash, so it is not served; nothing changed at the site's address. A publish from a computer answers it as FAILED at upload (422), naming the file. Publish the files again
site_file_missing / size_mismatch422FAILED at upload: a file in the publish was not sent to Gemmein, or the copy Gemmein received is a different size from the one the publish named. The answer names the file; nothing was put up. Run the publish again: it sends only what is missing
finalize_timeout422FAILED at upload: checking the files took longer than the time a publish has, and nothing was put up. Run the publish again: files already checked are kept and are not sent again
site_publish_cap429Development publishes each app's site up to 200 times a day, and today's are used. Try again after the time the message names. Batch several changes into one publish; a publish that changes nothing does not count
site_file_cap429Development takes up to 4,000 file puts a day for each app from a chat, and today's are used. Try again after the time the message names, or publish from a computer
site_rollback_cap429Development rolls each app's site back up to 100 times a day, and today's are used. Try again after the time the message names
site_revoke_cap429Development turns each app's previews off up to 20 times a day, and today's are used. Try again after the time the message names
not_published409This app has no site published, or nothing is served on the side asked for (rollback, rebuild, turning previews off). Publish first
already_offline409The live site is already offline. Rebuilding it, or putting a version live, brings it back
version_not_found404This site has no such version. npx gemmein site or site_status lists them
version_not_published409That version never went up (it failed, was discarded or is still waiting for its files). Pick a version that was served
version_files_removed410Gemmein keeps the files of the newest 50 versions and of what is served. This version keeps its history row but not its files, so it can't be rolled back to, promoted, rebuilt or downloaded. Publish again, or pick a newer version
approval_plan_changed409The Live site serves another version than the one you approved rebuilding. Nothing was rebuilt and the approval is not used. Ask again (site_rebuild with env live, or npx gemmein site rebuild --live)
link_expired403The site download link has expired. Ask for a fresh one: npx gemmein site pull, site_download or the Site room
platform_domain409The domain is the app's Gemmein address, which comes with its site and is not removed through domains. Your own domain can be added alongside it
publish_in_progress503Your preview is being updated: another update of this site is finishing. Try again in a minute
provider_unavailable503Gemmein couldn't reach the hosting network just now to update the preview. Try again in a few minutes
provider_unavailable502Changing or turning off the preview link: not everyone is cut off yet; ask again and Gemmein finishes it. Unpublishing the preview: Gemmein couldn't reach the hosting network and nothing changed; try again in a minute
publish_budget503A publish has a fixed time. Checking the files took too long to also test and put up the site in it. Publish again: files already checked aren't checked again
nothing_to_test409Testing a site: this app has no draft and no published site to test, the draft has no files yet, or it has no index.html. Open a draft with site_draft and put the files in
site_test_unavailable503Testing a site isn't switched on where this request was answered. Publishing still works
site_test_not_found404Reading a test run: no run with that id for this app. Reports are kept 7 days, the newest 20
site_test_running409A test of this site has been running longer than a test can. Try again in a minute; site_test_result shows the last runs
site_test_cap429Development tests a site up to 20 times a day for each app and 60 times a day for each account, and a publish is tested up to 200 times a day for each app. Today's are used. Try again after the time the message names
invalid_journey400Testing a site: journeys is not readable. A run walks at most 3 journeys of 12 steps; the message names the journey and step, and lists the steps the test reads
site_test_failed422FAILED at test: an uncaught JavaScript error in the draft's own files, or a page that never finishes loading, stopped the publish. The answer names the file and line and the fix; nothing was put up. Run site_test to see the page, fix it, publish again
site_test_failed_to_run502The test could not run. A publish goes ahead and says “Published UNTESTED” with the reason; site_test on its own says it could not run. Try again
site_test_no_pages503The test ran out of time before a page loaded, so nothing was tested. A publish answers publish_budget; publish again
preview_link_off409The preview link is off, so it has no link to change. Turn it on first; that makes a new link: site_share_link with on: true, npx gemmein site share --on, or Turn on on the Site page
share_link_changes_capped429Development changes each app's share link up to 20 times a day, and today's changes are used. The link you have still works; change it again after the time the message names
not_verified409Showing your site on a domain that isn't proved yours yet (domain_site, npx gemmein domains show, the domain's page). Add its TXT record and wait for it to verify, then show the site
email_only409Showing your site on a domain that only sends the app's email. Add the address you want the site at (like www.yourapp.com) as its own domain
site_errors_capped429A site sent its error reports for the hour (120 on Development, 600 on Live), or one visitor sent too many in a few minutes. More reports are dropped until the limit resets
site_errors_off403Error reports are switched off for this environment, so its pages' errors aren't collected right now
stale_report401An error report's time is more than 5 minutes from now, so it was refused
wrong_host403An error report came from an address that isn't this app's site, so it was refused

Approvals, the CLI and the connector

The codes npx gemmein login, the hosted connector (https://api.gemmein.com/mcp), each Live action, npx gemmein keys and npx gemmein domains can receive, and the ones the approval link's page and Settings → Approvals show. Your app's own requests never receive them. Each message names the fix.

CodeStatusMeaning · what to do
login_required401A Live action was asked for by a machine whose Development approval has ended or was revoked. Run npx gemmein login first; the approval lasts 7 days
local_rail404An approval was asked for on npx gemmein dev, which needs none. npx gemmein login signs this machine into your cloud app
invalid_machine400The approval request carries no machine name (up to 64 characters). The founder reads it on the page the approval link opens
invalid_scope400The approval request's scope is neither "development" (npx gemmein login) nor "live" (one Live action)
invalid_action400A Live approval request names an action Gemmein doesn't approve. The message lists the ones it does
invalid_target400A Live approval request is missing what it changes: the domain, the new name, the template or the key's name
plan_required400A Live approval request states no plan: the exact body it will send, or for sync --live each relay, tool and alias it will write
too_many_pending429Too many approval requests from this address or network are already waiting. Approve or cancel them on Settings → Approvals, or wait 10 minutes for them to expire
rate_limited429Too many approval requests, or too many codes tried, from this network, or more than 20 plan payment pages from one account in an hour (use the page already sent). resetAt says when to try again
slow_down429The CLI asked whether a request was approved more often than every few seconds. The answer carries the interval to wait; the CLI follows it
unknown_request404The CLI asked about a request Gemmein doesn't have. Run the command again for a fresh link and code
unknown_code404On the page the approval link opens: the code doesn't match a request. Check it against the one your terminal printed (a code lasts 10 minutes), or run npx gemmein login again
not_pending409On the page the approval link opens: the request can't be answered because it expired (a request lasts 10 minutes) or was already answered. Run the command again for a fresh code
pick_an_app400On the page the approval link opens: you have several apps. Choose which one this Development approval is for
wrong_app400On the page the approval link opens: a Live request is for the app that asked, and can't be approved for another
owner_only403Only the app’s owner switches Customer details for AI assistants (Settings → AI assistants); it is never switched from a chat or the CLI
owner_required403On the page the approval link opens: the email signed in to Gemmein doesn't own this app. Sign in with the email that owns it, then approve
approval_limit409On the page the approval link opens: 10 approvals already work for this app. Revoke one on Settings → Approvals, then approve this one
parent_ended409On the page the approval link opens: the machine that asked has lost its own approval (it ended or was revoked), so there is nothing to approve. Ask your AI to sign in again
not_active409On Settings → Approvals: the approval isn't working any more (it ended, was used, or was cancelled), so there is nothing to revoke
approval_key400On Settings → API keys: the key was handed to the CLI by a browser approval, so it isn't rotated or revoked there. Revoke the approval on Settings → Approvals; the key ends with it
live_approval_required403A Live action arrived without its approval: "this Live action needs your approval in the browser, each time — run it through the CLI (npx gemmein go-live, npx gemmein sync --live), which asks for it"
live_approval_used403That approval was already used; each Live action takes its own. Run the command again
live_approval_expired403That approval expired: an approval is good for 10 minutes after the click. Run the command again
live_approval_wrong_action403That approval was given for a different action; each covers exactly the one it names. Run the command again
approval_plan_changed403What your AI is sending differs from what you approved (or Development changed before it ran). Run the command again; it asks for a fresh approval
sign_in_required401The hosted connector was asked for a tool that needs your app before the AI app signed in. The answer carries a WWW-Authenticate challenge, and the AI app opens your browser on Gemmein's approval page; after Approve it repeats the call. Approve it, if you asked for it
invalid_token401The connector's access token is unknown, expired, or its approval was revoked or ran out (an approval lasts 30 days). The AI app asks you to sign in again; approve it again
insufficient_scope403A tool that reads Live was called on a connection without Live reads. The AI app asks for that approval; allow it only for an AI app you trust with your customers' data
key_in_browser403An AI app asked for a secret key's value in the chat. A secret key it asks for is made when you approve it in the browser and shown there once; it never comes back into the chat. Use the connector's request_server_key; paste the key yourself into the app host's secret settings
token_in_url400The connector's token was sent in the address. Send it in the Authorization header (Bearer), never in the address
host_refused403On npx gemmein dev: a request to the local /mcp was addressed to another host. The local runtime answers only requests to this computer (localhost or 127.0.0.1) or the address gemmein dev --host bound
host_not_allowed—A relay's call_url addressed a host that isn't on this app's outbound-host allowlist yet. The relay itself moves to waiting rather than failing outright — add the host at Settings → Hosts, and it retries on its own
origin_refused403On npx gemmein dev: a web page on another site called the local /mcp. Only pages served from this computer reach it
connector_unconfigured503The hosted connector or its sign-in isn't configured on the server that answered (no public address set for it). Nothing your AI app sends can fix it; on npx gemmein dev the local /mcp needs no sign-in. Write to hello@gemmein.com if https://api.gemmein.com/mcp answers it
answer_too_large200A JSON-RPC error inside a 200 answer from /mcp: the tool's answer is larger than the server sends at once. Use search_docs for one passage, or ask for less
parse_error400The body sent to /mcp isn't valid JSON (JSON-RPC error -32700). Send one JSON-RPC message, or a batch, as JSON
unsupported_media_type415A request to /mcp wasn't sent as JSON. Send JSON-RPC with content-type: application/json
invalid_request400A request to /mcp wasn't one JSON-RPC message or a batch of at most 10, or a connector sign-in request is missing what OAuth requires (such as the PKCE code_verifier, 43 to 128 characters). The connector's sign-in addresses answer 405 with the same error when called with the wrong method. Your AI app's MCP client sends these; update it, or connect again
gone410A connector sign-in link can't be used: it doesn't match a request, it was opened in a different browser from the one that started the sign-in, or the sign-in was already completed. Connect again from your AI app, or close the tab if it already finished
unknown_relay404A connector Live read named a webhook relay Live doesn't have. Check the relay's name on Live's Relays page
relay_not_found404An AI asked to delete a relay Live doesn't have, so nothing was asked of you. Check the name on Live's Relays page; a relay deleted in Development stays in Live until Live's own copy is deleted
ai_tool_not_found404An AI asked to delete an AI tool Live doesn't have, so nothing was asked of you. Check the name on Live's AI page
connector_error—A tool on the hosted connector couldn't finish: something in the input isn't the shape the guide teaches, or Gemmein hit an error of its own. Whatever was applied before it stopped stands. The AI calls pull to see Development as it is now, checks the input against the guide (search_docs), then tries again. See The hosted connector
interrupted—A Live action was approved and Gemmein was interrupted while carrying it out. live_action_status, the approval page and the CLI report it. When the answer says it may have gone through, check the place it names (Logs, the domain, the site) before asking again. Otherwise nothing changed and the approval can no longer be used. Ask again, and approve the new request
carried_out_by_approve409A Live request was polled for a one-use token, which only an older CLI does. Your Approve in the browser carries a Live action out, once, and the CLI reads what happened. Run npx gemmein@latest
seal_key_required400A Live key, or a Live push that makes secrets, was asked by a CLI that sent no key to seal them to. Nothing was asked of you. Run npx gemmein@latest and the command again
seal_key_required409A Live key was approved for a CLI that sent no key to seal it to, so the key was made and revoked at once. Run npx gemmein@latest and the command again
invalid_seal_key400sealTo on a Live request isn't the machine's X25519 public key (base64) for that request. Only a broken or foreign CLI sends one. Run npx gemmein@latest and the command again
not_retryable409Try again on the approval page for a request that can't run again: it is still being carried out, it finished, or it was already tried again. Read where the request stands on the page, or with live_action_status
nothing_to_retry409Try again on a request Gemmein doesn't carry out, so there is nothing to run again
not_carried_out409A Live action that no approval carries out. It is recorded as the reason on a failed answer from live_action_status and on the approval page
site_failed—A site promote or a Live rebuild failed. The site's own FAILED words, with the step, the file and the fix, are the reason on a failed answer from live_action_status and on the approval page
replace_every_key_setup_failed—Approving “Replace every key” (the API keys page, request_replace_every_key, or npx gemmein keys replace-every): Gemmein couldn't start replacing your Live key, so nothing was made or changed, the reason on a failed answer from live_action_status and on the approval page. Ask again
receipt_required403The CLI confirmed a hand-over without the receipt that came inside it, or with a wrong one. Only the machine holding the request's key can open the receipt. Run the command again
handover_expired410The CLI confirmed a hand-over after its approval ended. A Live key, or the secrets a Live push makes, that was never collected is revoked. Run the command again for a new one
nothing_handed_over404The CLI confirmed a hand-over on a request that made nothing for that machine
live_read_required403Reading Live's keys or the Live site's errors without the Live-read approval. Your human grants it: an AI app's connection asks again with its Read Live line, and the command line runs npx gemmein login --live-read. Every Live read is on Logs
step_not_found404The link of a key page or plan payment page isn't for this Gemmein account, or it has ended. Sign in with the account your AI uses, or ask your AI for a new link
step_expired409The link of a key page or plan payment page expired, and nothing changed. Ask your AI for a new link
step_closed409A key page had already ended, and nothing changed. Ask your AI for a new link
step_env_changed409A key page is for another environment than the one open now. Reload it
step_attempts429A key page has had its 10 tries. Ask your AI for a new link
step_rate_limited429One account tried more than 30 keys on key pages in an hour. Wait a little, then try again
wrong_step400A key page is for another kind of key (Stripe or an AI provider) than the one pasted. Open the page your AI sent for this key
step_used409A key was already saved on this page, so nothing more is taken from it
step_busy409A key is still being checked on the page. Try again in a moment
step_open_capped429An account opened 30 key pages in an hour. Use the page already sent, or try again later
invalid_provider400A key page was asked for a provider Gemmein doesn't connect. The provider is openai, anthropic, google, replicate, fal, assemblyai, deepgram, elevenlabs, runway or openai-compatible; a pipeline on your own server has no provider key, so set the tool's URL on its page
development_only_key409On a Development key page, the Use Development's key button was pressed. That page is Development's, and its key is pasted on it
door_error—A key page's last try hit an error on Gemmein's side while checking the key, and nothing was saved. Try again
last_call_failed—The problem a provider_key answer reports for a saved key whose last call failed. Make a test call from the key's page on the dashboard to see why
unknown_ai_tool404Deleting an AI tool with dev_delete_ai_tool: Development doesn't have a tool by that name. Check the name on the AI page
not_suspended409Lifting a suspension with test_member_unsuspend: the test member isn't suspended, and nothing changed
dashboard_only403Asked for something an AI never does from a chat: customer details for AI assistants (Settings → AI assistants), revealing or rotating keys, Stripe settings (made in Stripe; the key itself on Payments), sender domains (Domains), or deleting the app or the account. Each is yours to do on the dashboard. The same answer comes for a team or ownership change (an account is one person, so none applies), a retention setting (retention is fixed), or a bulk export, bulk record change or bulk email (Gemmein sends no bulk email). Nothing changed
refund_in_stripe—A refund is never made from a chat. The answer gives you the exact steps: the amount, the customer and the payment in Stripe → Payments → Refund. Once Stripe confirms it, a full refund ends what the payment unlocked and takes back its credits; a part refund leaves what it unlocked in place and takes back its credits in proportion
already_refunded409The payment is already refunded in full. Nothing to do
already_suspended409The customer is already suspended. Nothing was asked
nothing_to_cancel409The customer has no paid subscription to cancel
subscription_from_relay409The customer's plan came from a relay (grant_plan), so the system that sent that event decides it — cancel it there, not here
subscription_unknown409The customer's plan has no Stripe subscription on record to cancel. Cancel it in Stripe; Gemmein hears it from Stripe's webhook
reply_has_secret400A reply to a customer holds something shaped like a secret key. A key never goes to a customer; nothing was asked or sent — rewrite the reply without it
record_not_found404An AI's update_record, delete_record or publish_record named a record id Live doesn't have in that collection, so nothing was asked of you. Check the record's id on Live's Data page
relay_event_not_found404No relay run with that id in Live. relay_runs lists them
relay_event_busy409The run is still queued or running. Only a finished run (done, failed or dead) runs again
no_approval_needed409Your AI asked you to approve a Development key. A Development key needs no approval: npx gemmein keys create … --env development makes it at once under this machine's approval, and Logs record it

Your dashboard app code

The codes the dashboard's sign-in and its Live changes to how the business runs can answer once you turn on an authenticator app (Settings → Security). Your app's own requests never receive them. Lost your phone and your recovery codes? Email support@gemmein.com from your account's address; support checks it by hand.

CodeStatusMeaning · what to do
app_code_required403This act needs the code from your authenticator app (6 digits, a new one every 30 seconds), or a recovery code: every Live change to how the business runs (a promote or a Live push, a domain, keys, deleting a collection, relay, AI tool or email template, the Live site), on the dashboard or approving it for an AI, plus deleting the account and turning the app code off. A change to one customer never asks it, and neither does going live. Enter it and try again
app_code_invalid401"That code didn't work. Use the newest code from your authenticator app — each one works once — or a recovery code."
app_code_sign_in_expired401The sign-in waited more than 5 minutes for the app code, or that step was already used. "Start again with a new email code."
app_code_wait429Ten wrong codes in a row: the next try waits (1 minute, then 5, 15, 1 hour and longer, up to 24 hours) and is not checked before then. "Too many tries. Try again in …, or use a recovery code — it works now." A right code after the wait always works, so the account never locks
app_code_already_on409Your authenticator app is already on. To change it, turn it off first, which asks for a code from it or a recovery code
app_code_not_pending409Setup was never started, or the code you scanned has been replaced. Start setup again
app_code_not_on409Your authenticator app isn't on, so there is nothing to turn off
app_code_spend_unrecorded503Gemmein couldn't record a spend from the dashboard's own app code just now, and nothing was changed. Try again in a moment

Test kit

The codes the test kit's commands receive: npx gemmein members, mail, approve, send-back, suspend, sign-out, pay and refund. The kit works in Development, on test members only. From a chat, the hosted connector’s prove_app tool signs in as two test members on the server and proves the app: each person sees only their own records, its relays fire (their email lands in the test mailbox), and each AI tool answers and spends its credits.

CodeStatusMeaning · what to do
prove_needs_test_members409The connector’s prove_app tool: a proof signs in as two test members, and Development has fewer than two. Add them first (test_member_add alice, test_member_add bob, or npx gemmein members add alice). It also answers when Development has no public key to sign test members in with: open the app's Development on the dashboard once, then try again
test_kit_development_only403A kit command in Live. Nothing changed: Live's people are real, so approving, suspending and signing out there are done on the dashboard
test_members_full409Development holds 20 test members. Reuse one; npx gemmein members lists them
test_kit_capped429The kit's daily allowance in Development is spent: 200 changes, 20 test checkouts or 20 refunds. resetAt says when it opens again
not_a_test_member403The command names a real person, or a post or payment of one. The kit acts on test members only; real members are handled on the dashboard, and real payments are refunded in Stripe
not_moderated409approve or send-back in a collection whose posts publish when their author says so. Set "publishing": "owner approves" in its file to review posts first
not_waiting409The post isn't waiting: it is already published, was sent back, or its author took it down. A post sent back waits again once its author edits it
changed_since_read409The post changed since it was read. Run the command again to read it afresh, then decide
stripe_not_connected409pay or refund: Development has no Stripe key. Connect a sandbox (rk_test_) key on Payments first
test_pay_live_key_refused403pay or refund: Development's Stripe key isn't a test-mode (sandbox) key, so nothing was charged or refunded. The kit never touches live money
test_pay_local409pay on npx gemmein dev, which simulates checkout itself with no Stripe and no money: as the member, g.payments.buy() and g.subscriptions.checkout() return a link that completes the purchase when opened. Test real Stripe in your cloud app's Development
unknown_item404pay: the item isn't a product or paid plan sold in Development; the message names what is
payment_not_found404refund: no payment with that reference in Development yet. It appears once Stripe's webhook has fulfilled it, which npx gemmein pay waits for
test_refund_refused409refund: Stripe refused the refund; the message gives Stripe's reason. If the key lacks the refund permission, the answer is stripe_key_permissions, naming Charges and Refunds: Write
test_refund_outcome_unknown502refund: Stripe didn't answer, so the outcome is unknown and the refund may have been made. Check Payments in Logs before trying again; the same refund run again within 24 hours never refunds twice
test_checkout_unknown502pay: checking a slow test payment with Stripe failed; the message gives Stripe's reason. Check Payments in Logs, then try again in a moment
test_pay_unavailable501This server can't run test payments or refunds through Stripe

Only on the Errors page

Your app's requests are never answered with these. Each one is a row on the dashboard's Errors page, in the environment it happened in, carrying a sentence that names what happened.

CodeStatusMeaning · what to do
dev_key_on_live_site—In Live, once a day per domain: a page on one of the app's verified domains still calls with the Development key, so customers there reach Development data. "Your live site <domain> is still using the Development key, so customers there reach Development data." npx gemmein domains and npx gemmein check print it too. Run npx gemmein keys app and switch the live build to the Live key (also on Settings → API keys, Live). A preview address that is not a verified domain never raises it
relay_unsigned_event—A relay acts on events its sender doesn't sign, without the opt-in, so every event is refused: "this relay acts on events its sender doesn't sign — every event is refused until you set trigger.trustUnsignedEvents: true on it, or keep it to write_record and call_url". See Unsigned events
relay_key_as_secret—A relay that checks a shared secret holds a public key, so every event is refused: "this relay's stored value is a public key; paste the sender's signing secret". Paste the sender's signing secret on the relay's page
relays_delayed—In Live, a relay's events waited more than 30 seconds to run. They are kept and run in order; the message says whether your own relays were at their limit. See Relays
relays_held—A relay's event waited more than 60 seconds behind an earlier event for the same record or person; the message names the relay it waits behind and why. Resume or fix that relay
relays_dev_capped—A Development app passed 120 immediate relay runs in an hour (300 across your account); the rest run within minutes, none dropped. For volume, run on npx gemmein dev
email_suppressed—10 of your customers' addresses have bounced or marked your mail as spam, and again at 20, 30 and so on. Gemmein no longer mails those addresses. Check how addresses are collected and what your emails say
inbound_capped—Inbound mail passed this hour's limit (20 from one sender, or 300 from people who aren't your customers); the rest was dropped, not stored. Usually a mail loop or spam. Inbound mail belongs to no environment, so this row is in your app's default environment
stripe_wrong_mode—Stripe sent a test-mode event to Live's address, or a live-mode one to Development's. Nothing was written. Point the Stripe webhook at the right environment
stripe_payments_closed—A payment arrived after payments were turned off. Nothing was granted; refund it in Stripe or grant it by hand
stripe_payment_unresolved—A payment arrived that no plan or product can be named for. Nothing was granted; grant it by hand or refund it in Stripe
stripe_account_in_use—This Stripe account is connected to another Gemmein account, so payments here can't be changed. Connecting that account's key on the Payments page is refused with the same code (409)
stripe_connection_broken—Gemmein's checks found your Stripe key revoked or changed, or its webhook deleted, disabled, pointed elsewhere or missing events. Open Payments to fix it
payment_erased_payer—A payment for an address whose owner deleted their account. Nothing was granted; refund it, or grant it by hand if it is a new purchase
erased_subscription_active—A deleted customer's Stripe subscription is still running. Gemmein can't cancel it for you; cancel it in Stripe
checkout_ref_invalid—A checkout didn't come from your app's checkout for this app, environment and plan, so it granted nothing. Customers buy through your app; refund or cancel it in Stripe
trial_ref_invalid—A free-trial checkout didn't come from your app's trial offer, so it granted nothing. Cancel it in Stripe
trial_already_used—The customer already had a free trial or a paid plan here, so this trial grants nothing. Cancel it in Stripe
duplicate_subscription—One customer has two live subscriptions in Stripe. Gemmein can't cancel in Stripe; cancel one there
refusals_not_logged—More than 200 refused purchases from signed-out visitors in one hour; the rest of that hour were counted, not listed one by one

Client-side

CodeStatusMeaning · what to do
key_scope_mismatch0 (client-side)gemmeinServer(sk, { expects }) compared the key's access with what the app needs before its first call, and the key has less or more. A key too broad is refused like a key too narrow. err.missing and err.extra say what, one plain sentence each ("this key cannot write mentor, which the app needs"). Make a key with exactly that access (npx gemmein keys create) and swap it in; revoke the old one
secret_key_in_client0 (client-side)A secret key (sk_) was handed to gemmeinServer() running in a browser or a mobile app; it refuses to run there. Keep the key on your own server (an API route, a server action, a worker) and call that from the app
invalid_secret_key—gemmeinServer() got a missing or pk_ key; pass the sk_ from a server env var
invalid_response0 (client-side)The SDK's own, with no HTTP status: a 200 from verifyEmailCode() that carried no session token, or a provider answer with no text where g.ai.text() or g.ai.runText() expected one (a streaming body: read the stream with g.ai.chat() or g.ai.run())
network_unreachable0 (client-side)The request never reached Gemmein: no connection, an offline device, a host that does not resolve, an apiUrl pointing at nothing. The message names the host and err.cause carries the transport's own error. Check the connection and the apiUrl; the call is safe to retry. A call cancelled with { signal } stays an AbortError
upload_input_unsupported0 (client-side)@gemmein/sdk/expo: upload() was handed a picker's { uri } and expo-file-system is not installed, so there is nothing to read the bytes with. Install it (npx expo install expo-file-system), or pass a Blob or an expo-file-system File
token_too_large0 (client-side)@gemmein/sdk/expo: a value over 2,048 bytes was handed to the device secure store, which iOS refuses with a native throw the app cannot read. Store the token, not the session payload. A Gemmein session token is two orders of magnitude smaller. See Mobile
secure_store_unavailable0 (client-side)Sign-in couldn't save the session token, so it throws instead of returning a session that would be lost on the next launch. All three of the SDK's stores raise it:
  • BrowserTokenStore (@gemmein/sdk): the browser refused the localStorage write (private mode past its quota, site data blocked, a sandboxed iframe).
  • @gemmein/sdk/expo: expo-secure-store is not installed, or the device refused the write.
  • Swift KeychainTokenStore: the Keychain refused, and the OSStatus is in the message. -34018 is an unsigned build with no keychain access group, so sign the app (ad-hoc is enough).
The store's own error is on err.cause (in Swift, in the message). Reads and clears stay lenient on all three: an unreadable store means signed out, never a crash. The session itself is valid, so an app that should keep running catches this one code and rebuilds its client with a MemoryTokenStore. See Mobile
invalid_collection_name0 (client-side)g.collection(name) was given a name outside the grammar: lowercase letters, digits and underscores, starting with a letter, 2 to 63 characters (tasks, user_notes). Thrown synchronously, before any request exists, and the message echoes what it was given. A name that is legal but has no collection behind it is a different code (unknown_collection, 404)
aborted0 (client-side)g.runs.watch stopped because its signal was aborted (in Swift, the calling Task was cancelled). The run itself carries on; call g.runs.get to read it
Was this page helpful?

Last updated