Skip to content

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.


{
"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
}
}
  • status is always "success".
  • data carries the payload (list, object, or scalar). Never null on success.
  • pagination is present only when the caller passed page or offset.
  • There is no token key. Login and refresh return accessToken / refreshToken inside data.
  • 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
}
  • status is always "error".
  • error is the stable machine code (see the table below). Branch on this, not on message.
  • message is human-readable and may change.
  • data is always null.
  • details is 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.

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 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 data slot 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 (a create_* / update_* field whose target is not a model) is VALIDATION_ERROR (400) – it cannot be told apart from a typo. A syntactically malformed query is VALIDATION_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_graphql and after_graphql run 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.

  • 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 a role:"*" public model with a stale token must refresh-and-retry or omit the Authorization header.
  • A syntactically malformed GraphQL query now returns HTTP 400 VALIDATION_ERROR (Invalid GraphQL syntax: ...) instead of HTTP 500 INTERNAL_ERROR.
  • Rate-limited responses (HTTP 429) now use the canonical error envelope instead of the legacy {success, error} shape.
  • _bulk_update where every record fails now returns HTTP 400 VALIDATION_ERROR (with details.failed_records) instead of HTTP 200 with a statusCode: 400 mismatch. This matches _bulk_create.
  • Pure-input failures on write / filter operations now return VALIDATION_ERROR (400), not PERMISSION_DENIED (403): No data provided for creation/update, ... requires query filters for safety (delete / destroy / exists), No valid filter conditions provided, and No valid WHERE conditions provided.
  • A non-bulk PUT /{model}/update with no where filters now returns HTTP 400 VALIDATION_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 matches delete / destroy / restore.
  • /auth/* and /storage/* now use this canonical envelope (was the legacy {success, status_code} shape). The data payloads 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/create now returns HTTP 200 (was 201), matching the rest of the contract (creates are 200).
  • /storage/upload/image and /storage/upload/file now require a Bearer token (they previously accepted anonymous uploads).