Response Envelope
Every kirak-core response – CRUD, GraphQL, /auth/*, /storage/*, success or
failure – uses one envelope. The HTTP status line always equals the statusCode
field in the body, so a client can trust either one. Rate-limited responses
(HTTP 429) use it too.
For the full endpoint-by-endpoint contract (every route, request/response shape, filter operators, pagination, auth + storage flows) see the HTTP API Reference.
Success
Section titled “Success”{ "statusCode": 200, "status": "success", "message": "Fetched 20 records", "data": <payload>, "pagination": { "limit": 20, "page": 1, "offset": 0, "total": 156, "total_pages": 8, "has_more": true }}statusis always"success".datacarries the payload (list, object, or scalar). Nevernullon success.paginationis present only when the caller passedpageoroffset.- There is no
tokenkey. Login and refresh returnaccessToken/refreshTokeninsidedata. - Every 2xx keeps HTTP 200 (create is not 201, delete is not 204).
{ "statusCode": 403, "status": "error", "error": "PERMISSION_DENIED", "message": "Role 'user' is not authorized to destroy 'posts'", "data": null}statusis always"error".erroris the stable machine code (see the table below). Branch on this, not onmessage.messageis human-readable and may change.datais alwaysnull.detailsis included only when the failure carries structured context, e.g."details": {"field": "email"}or"details": {"failed_records": [...]}.- HTTP status always equals
statusCode. Auth failures are 401/403, not 200.
Machine codes
Section titled “Machine codes”Core codes can come from any route; a module’s codes come from its own routes. Modules
not listed (payments, notifications, ai, …) also answer codes of their own, not
catalogued yet; statusCode gives their class. GET /docs lists the codes of the
running app.
error |
Status | From | Meaning |
|---|---|---|---|
VALIDATION_ERROR |
400 | core | The request, a field value or a GraphQL query is invalid, including a duplicate of a unique field. |
AUTHENTICATION_ERROR |
401 | core | A credential is required or was rejected. |
PERMISSION_DENIED |
403 | core | The caller’s role may not do this, or not on these records or fields. |
NOT_FOUND |
404 | core | No such route, model or record, or a valid token’s user no longer exists. |
RATE_LIMIT_EXCEEDED |
429 | core | Too many requests; retry after the Retry-After header’s seconds. |
DATABASE_ERROR |
500 | core | The database operation failed; the details are logged, not returned. |
INTERNAL_ERROR |
500 | core | An unexpected server error. |
HTTP_ERROR |
varies | core | Any other HTTP error; see statusCode. |
MISSING_TOKEN |
401 | auth | The route needs a credential and none was sent. |
INVALID_TOKEN |
401 | auth | The access or refresh token is malformed or not valid. |
TOKEN_EXPIRED |
401 | auth | The token has expired: refresh an access token, sign in again for a refresh token. |
TOKEN_REVOKED |
401 | auth | The token was revoked (logout, password change); sign in again. |
AUTH_UNAVAILABLE |
503 | auth | The token blacklist could not be read or written, so the token was not accepted (or the logout not completed); retry. |
INVALID_API_KEY |
401 | auth | The API key is unknown, revoked or expired. |
INVALID_CREDENTIALS |
401 | auth | Wrong email or password, or a wrong current password on change-password. |
INVALID_OTP |
401 | auth | The one-time code is wrong or expired. |
INVALID_MFA_CODE |
401 | auth | The MFA or backup code is wrong. |
SOCIAL_AUTH_FAILED |
401 | auth | The social provider sign-in did not complete. |
MOBILE_SIGN_IN_NOT_SUPPORTED |
400 | auth | Mobile sign-in accepts only apple-id, facebook and google-oauth2, whose tokens Kirak can check were issued to this app. |
SOCIAL_EMAIL_NOT_VERIFIED |
409 | auth | An account has the email of this social sign-in, and the provider did not confirm the email is verified, so it is not linked. |
MFA_REQUIRED |
403 | auth | The account has MFA: repeat the login with mfa_code. |
ACCOUNT_INACTIVE |
403 | auth | The account is deactivated. |
EMAIL_NOT_VERIFIED |
403 | auth | The email address must be verified before signing in. |
WEAK_PASSWORD |
400 | auth | The new password does not meet the password rules; message says which. |
INVALID_RESET_TOKEN |
400 | auth | The password reset link is invalid or already used. |
EMAIL_ALREADY_EXISTS |
409 | auth | An account with this email exists. |
USER_DATA_ERROR |
500 | auth | The signed-in user’s record could not be read. |
STORAGE_ERROR |
500 | storage | The storage provider failed to delete, sign a URL for or list files. |
GraphQL
Section titled “GraphQL”GraphQL returns the same envelope. It is not spec-compliant GraphQL-over-HTTP:
there is no top-level errors[].
- Any error in any root field raises and fails the whole request. There is
never a partial
dataslot with a per-field{"error": ...}entry. - The error renders through the same handler as REST, with the exception’s own
status code. An unknown model is
NOT_FOUND(404), matching REST. An unknown mutation verb (acreate_*/update_*field whose target is not a model) isVALIDATION_ERROR(400) – it cannot be told apart from a typo. A syntactically malformed query isVALIDATION_ERROR(400) (Invalid GraphQL syntax: ...). - GraphQL runs per-model CRUD hooks (
before_create,after_fetch, etc.) for every sub-operation it executes, exactly as REST does. In addition,before_graphqlandafter_graphqlrun once per GraphQL request – registered on the"graphql"model name, not on individual model names. - A multi-root GraphQL mutation runs all root fields inside one database transaction. If any field fails, the entire mutation rolls back – earlier fields are not committed. GraphQL queries (reads) do not open a transaction.
Behavior changes
Section titled “Behavior changes”- A present-but-invalid or expired JWT now returns HTTP 401 (
TOKEN_EXPIRED/INVALID_TOKEN) instead of silently degrading to guest access. A client reading arole:"*"public model with a stale token must refresh-and-retry or omit theAuthorizationheader. - A syntactically malformed GraphQL query now returns HTTP 400
VALIDATION_ERROR(Invalid GraphQL syntax: ...) instead of HTTP 500INTERNAL_ERROR. - Rate-limited responses (HTTP 429) now use the canonical error envelope instead
of the legacy
{success, error}shape. _bulk_updatewhere every record fails now returns HTTP 400VALIDATION_ERROR(withdetails.failed_records) instead of HTTP 200 with astatusCode: 400mismatch. This matches_bulk_create.- Pure-input failures on write / filter operations now return
VALIDATION_ERROR(400), notPERMISSION_DENIED(403):No data provided for creation/update,... requires query filters for safety(delete / destroy / exists),No valid filter conditions provided, andNo valid WHERE conditions provided. - A non-bulk
PUT /{model}/updatewith nowherefilters now returns HTTP 400VALIDATION_ERROR(Update operation requires 'where' filters for safety) instead of logging a warning and proceeding. To update every row, pass an explicit always-true filter. This matchesdelete/destroy/restore. /auth/*and/storage/*now use this canonical envelope (was the legacy{success, status_code}shape). Thedatapayloads are unchanged – only the wrapper keys moved (success/status_code->statusCode/status). Auth failures carry the specific codes in the “Auth machine codes” table.POST /auth/api-keys/createnow returns HTTP 200 (was 201), matching the rest of the contract (creates are 200)./storage/upload/imageand/storage/upload/filenow require a Bearer token (they previously accepted anonymous uploads).