Errors
Every Koolbase error speaks one shape: a human-readable message plus a stable, machine-readable code. Branch on the code — never on the message text.
The error shape
Any non-2xx response is a flat JSON object with two fields, plus an optional details:
{
"error": "invalid email or password",
"code": "invalid_credentials"
}error— a human-readable message, for logs and quick debugging. Wording can change between releases; don't parse it.code— a stable, snake_case identifier you can switch on. This is the contract.details— present on a few codes that carry structured context (see below).
Code over message
code is the part we keep stable across releases. Matching on the error string is brittle — a copy-edit to a message would silently break your handling. Switch on code.Structured details
Some codes attach a details object. A unique_violation names the field that collided, so you can point the user at the right input:
{
"error": "value violates a unique constraint",
"code": "unique_violation",
"details": { "field": "email" }
}When you declare a unique constraint on a collection that already holds duplicates, the rejection carries details.duplicates — the offending value groups, so you can dedupe before retrying.
Error codes
Every code is global and stable. The defaults below apply to any endpoint based on the HTTP status; specific endpoints emit the more precise codes that follow.
Four codes are deliberately absent from these tables — error, invalid, duplicate and state_conflict. They carry nothing an application can act on, and a more specific code applies wherever one exists. Treat them as you would an unrecognised code.
Defaults
| Code | HTTP | Meaning |
|---|---|---|
| unauthenticated | 401 | Missing or invalid authentication (no/expired token). |
| permission_denied | 403 | Authenticated, but not allowed to perform this operation. |
| not_found | 404 | The requested resource does not exist. |
| conflict | 409 | Generic conflict with the current state. |
| rate_limit | 429 | Too many requests — back off and retry. |
| validation_error | 400 | The request was malformed or failed validation. |
| internal_error | 5xx | An unexpected server error. |
Database
| Code | HTTP | Meaning |
|---|---|---|
| unique_violation | 409 | A write violates a unique constraint. Carries details.field. |
| collection_not_found | 404 | No collection with that name. |
| record_not_found | 404 | No record with that id. |
| ambiguous_match | 409 | An upsert matched more than one record. |
| duplicate_values | 409 | Declaring a constraint, but existing rows already violate it. Carries details.duplicates. |
| constraint_exists | 409 | A unique constraint already exists for those fields. |
| constraint_not_found | 404 | No such unique constraint. |
| reference_invalid | 400 | A reference field points at a record that does not exist, is deleted, or is in another collection. Checked when the write commits, so it can come from an insert, an update or a batch — a batch is refused whole. |
| reference_in_use | 409 | Deleting a record that live records still reference, under a reference declared on_delete: restrict. Delete the referencing records first, or in the same batch — the check runs at commit, so one batch may delete a parent and its children in any order. |
| dangling_references | 409 | Declaring a reference over data that already points at missing records. details.dangling lists the offending records (first 50) so you can repair them; Koolbase never repairs them for you. |
| collection_referenced | 409 | Deleting a collection another collection's reference field points at. Remove that reference first. |
| revision_mismatch | 409 | The record moved between composing the change and the write arriving. Carries both revisions and the current record, so the conflict can be resolved without fetching again. |
| idempotency_key_reused | 409 | The same idempotency key sent with different data — refused rather than replayed. |
| idempotency_conflict | 409 | The fiscal counterpart of idempotency_key_reused: a client_ref that exists with a different payload. |
| batch_failed | 400 | A batch write failed for a reason the server did not classify further. |
| insufficient_authority | 403 | Not permitted to perform a destructive operation such as a seed overwrite or snapshot restore. Unlike insufficient_scope, this is about the operation rather than the key. |
| invalid_body | 400 | The request body could not be decoded. |
| no_changes | 400 | The request asks for no change. |
Vectors & embeddings
| Code | HTTP | Meaning |
|---|---|---|
| vector_dimension_mismatch | 400 | The vector's dimension does not match the field declaration — usually a changed model, or the wrong model producing the embedding. |
| vector_not_found | 404 | No vector on that record for that field. |
| vector_field_not_found | 404 | The collection has no vector field with that name. |
| vector_field_exists | 409 | A vector field with that name already exists on the collection. |
| vector_collection_mismatch | 400 | The record's collection does not match the vector field's collection. |
| unsupported_dimension | 400 | That vector dimension is not supported. |
| invalid_embedding_config | 400 | provider, model and source_field must be set together or all cleared — partial config is refused rather than half-applied. |
| field_not_auto_embed | 400 | A backfill was asked for on a field that embeds nothing automatically. |
| provider_not_configured | 400 | No embedding provider is configured; embedding runs on the project's own provider key. |
| provider_invalid | 400 | The embedding provider's credentials were rejected. |
Authentication
| Code | HTTP | Meaning |
|---|---|---|
| invalid_credentials | 401 | Wrong email or password. |
| email_in_use | 409 | That email is already registered. |
| email_not_verified | 403 | Email must be verified before sign-in. |
| account_locked | 429 | Too many failed attempts; temporarily locked. Honour the Retry-After header. |
| account_disabled | 403 | The account has been disabled. |
| invalid_refresh_token | 401 | Refresh token rejected — the session is over; sign in again. |
| invalid_unlock_token | 400 | The unlock-email token is invalid, expired, or already used. |
| invalid_token | 400 | A verification or password-reset token is invalid or expired. |
| token_used | 400 | That token has already been used. |
| token_expired | 400 | That token has expired — request a new one. |
| invitation_invalid | 400 | The invitation has been revoked or expired. |
| oauth_not_configured | 400 | Apple/Google sign-in is not configured for this environment. |
| invalid_oauth_token | 401 | The Apple/Google identity token failed verification. |
| oauth_email_required | 400 | The provider did not return an email for this sign-in. |
| oauth_email_conflict | 409 | The email belongs to an existing account; sign in and link from settings. |
| invalid_phone | 400 | Phone number is not valid E.164. |
| otp_expired | 400 | The OTP has expired. |
| otp_invalid | 400 | The OTP code is incorrect. |
| otp_max_attempts | 400 | Too many incorrect OTP attempts. |
| phone_in_use | 409 | That phone number is linked to another account. |
| sms_not_configured | 400 | No SMS provider is configured for this project. |
| weak_password | 400 | Password must be at least 8 characters — the server's rule, which a project may set higher than the SDK checks. |
| invalid_password | 400 | A password change was refused because the CURRENT password is wrong. |
| signups_disabled | 403 | New registrations are off for this project. |
| email_code_disabled | 403 | The project has switched off signing in with an emailed code. Applies to every address alike. |
| account_exists | 409 | That email already has an account — sign in with its existing method, then connect this provider. |
| contact_not_verified | 403 | Email or phone must be verified before sign-in, because the project requires verified contact. |
| last_credential | 409 | Cannot remove the last sign-in method — the account would have no way in. |
| resend_cooldown | 429 | Another verification email was asked for too soon. Carries cooldown_until. |
| resend_daily_cap | 429 | The daily verification-email limit is reached. |
| hide_requires_verification | 400 | hide_account_existence needs require_verified_contact enabled. |
| oauth_only_account | 403 | This account signs in with Google or Apple and has no password. |
| unsupported_oauth_provider | 401 | That sign-in method is not available. |
| identity_not_found | 404 | That provider is not connected to this account. |
| provider_identity_already_linked | 409 | Another account already holds that Google or Apple identity. |
Storage
| Code | HTTP | Meaning |
|---|---|---|
| path_conflict | 409 | An object already exists at this bucket+path and overwrite was false. Uploads are safe-by-default; pass overwrite: true on the upload-url request to replace. |
| quota_exceeded | 409 | Upload would push the bucket past its configured max_size_bytes. |
| file_too_large | 413 | Single file exceeds the bucket's configured max_file_size_bytes. |
| mime_not_allowed | 415 | File content_type is not in the bucket's allowed_mime_types list. Wildcards like image/* match the family; an empty allowed list means no restriction. |
| upload_expired | 410 | Confirm was called after the upload grace window (~1 hour) and the presigned URL is too old to honor. Request a new upload URL and try again. |
| metadata_invalid | 400 | Metadata failed validation. A top-level detail names the failing key and the rule it broke — read it there, not under details. |
| cap_below_usage | 400 | A bucket cap set below what the bucket already holds; the message carries both figures. |
| upload_url_failed | 400 | Minting a presigned upload URL for a template asset failed. Templates rather than object storage, despite the name. |
Platform & billing
| Code | HTTP | Meaning |
|---|---|---|
| plan_limit_reached | 402 | The operation would exceed your plan's limit. Carries details: { resource, limit, plan }. Existing resources are never affected — only the new create or new usage is refused. |
| insufficient_scope | 403 | The API key's scope is below what this operation requires (read < write < admin). The message names the required scope. |
| session_required | 403 | This operation requires a dashboard session — API keys cannot perform it (e.g. minting new API keys). |
Projects & seeding
| Code | HTTP | Meaning |
|---|---|---|
| project_invalid | 400 | Invalid project id. |
| slug_taken | 409 | A project with this slug already exists. |
| invalid_seed_file | 400 | The seed file is not valid; details carry a problems list. |
| seed_key_not_unique | 400 | The seed file's key does not identify rows uniquely; the message names the problem. |
| seed_needs_decision | 409 | The seed cannot proceed until the caller chooses how to handle conflicts. |
| seed_conflicts_require_force | 409 | Rows changed in both the seed file and the target; overwriting discards the target's version. details carry the rows. |
Handling errors in the SDK
The SDKs do the code matching for you — they throw typed exceptions selected from the code, so your app catches a class instead of inspecting status or message. Data-layer failures share a base (KoolbaseDataException) you can catch broadly, or you can catch a specific subtype:
try {
await Koolbase.db.collection('users').insert({'email': email});
} on KoolbaseConflictException catch (e) {
// e.field is the collided field, when the server reports it
showError('That ${e.field ?? 'value'} is already taken.');
} on KoolbasePermissionException {
showError('You do not have permission to do that.');
} on KoolbaseDataException catch (e) {
// Catch-all for any other data-layer error
showError(e.message);
}Auth methods throw KoolbaseAuthException subtypes — InvalidCredentialsException, AccountLockedException, OtpExpiredException, and so on — selected from the same codes.
Storage uploads have their own typed exception family — KoolbaseStorageConflictException, KoolbaseStorageQuotaException, KoolbaseStorageFileTooLargeException, KoolbaseStorageMimeTypeException, KoolbaseStorageNotFoundException, KoolbaseStoragePermissionException, KoolbaseStorageValidationException — all extending KoolbaseStorageException. Catch the base to handle any storage failure, or branch on the specific subclass. The upload_expired code surfaces on the base class without a dedicated subclass; check .code if you need to distinguish it.
React Native
Error-based classes — KoolbaseDataError (base), KoolbaseConflictError (with .field), KoolbaseNotFoundError, KoolbaseValidationError, KoolbasePermissionError, KoolbaseRateLimitError, and KoolbaseAuthError subtypes. Storage gets KoolbaseStorageError (base), KoolbaseStorageConflictError, KoolbaseStorageQuotaError, KoolbaseStorageFileTooLargeError, KoolbaseStorageMimeTypeError, KoolbaseStorageNotFoundError, KoolbaseStoragePermissionError, KoolbaseStorageValidationError. Catch with instanceof. The optimistic write paths (insert, update, delete) sync in the background, so their conflicts surface through the sync engine rather than as a thrown error; query, get, upsert, and deleteWhere throw the typed errors directly.