Hashboard’s service layer uses a set of typed error classes that map directly to HTTP status codes. Every route handler is wrapped by theDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/cryguy/hashboard/llms.txt
Use this file to discover all available pages before exploring further.
api() utility, which catches these typed errors and serializes them as JSON — route handlers contain no try/catch of their own. Any unrecognized exception propagates as a 500.
Error Response Shape
All error responses return JSON with anerror field containing a human-readable message. The Content-Type is always application/json.
409 Conflict response on document saves includes an additional field:
HTTP Status Codes
| Status | Error Class | When it occurs |
|---|---|---|
400 | ValidationError | The request body fails Zod schema validation, the JSON is malformed, or a semantic check in the service layer fails (e.g. missing ?filename= on an upload, empty upload body, invalid date string). |
401 | (unauthenticated) | No valid token or cookie was presented, or an anonymous request targeted a resource not on the open-path or anonymous-sharing allowlist. Also returned for private resources an anonymous caller requests — existence is never confirmed. |
403 | ForbiddenError | The caller is authenticated and can see the resource but is not authorized to perform this action. Examples: editing a doc you have read-only access to, changing visibility on a card you did not create, or attempting to demote the superadmin. |
404 | NotFoundError | The resource does not exist. Also thrown deliberately for resources the actor may not see — Hashboard never confirms whether a private object exists to a caller who cannot access it. |
409 | ConflictError | A stale baseVersion was supplied to PUT /api/v1/docs/:id. The response body includes currentVersion so the caller can re-fetch and retry. Also returned for duplicate label names on POST /api/v1/labels and PATCH /api/v1/labels/:id. |
429 | RateLimitError | Too many requests. Applied to login (per username+IP), registration (per IP), and anonymous file uploads (per IP). A successful login clears the login window for that username+IP pair. |
500 | (unexpected) | Any error that does not match a typed service error class. These are bugs — the typed errors cover all expected failure modes. |
The 401 vs 404 distinction is intentional. A caller who cannot see a resource receives a 401 for it, not a 404 — returning 404 would confirm that the resource does not exist, which would leak information about private objects. Only callers who already have read access receive a genuine 404 for a resource that is truly absent.
Validation Errors (400)
Validation errors are generated in two places:- Schema validation —
readBody(event, zodSchema)parses the JSON body and runs it through the Zod schema. If the shape is wrong, the error message includes the field path and the issue:"columnId: Required","baseVersion: Expected number, received string". - Service-layer checks — semantic constraints that Zod cannot express, such as checking that
prevIdandnextIdbelong to the target column on a move operation, or that a?filename=query parameter was provided for an upload.
Conflict Errors and Optimistic Concurrency (409)
Document saves use optimistic concurrency. When you fetch a document — viaGET /api/v1/docs/:id, GET /api/v1/cards/:id, or the .md rendition — the current version integer is included in the response. You must pass that value as baseVersion when calling PUT /api/v1/docs/:id. If another writer saved the document between your read and your write, the UPDATE … WHERE version = baseVersion matches zero rows and Hashboard responds with 409.
The currentVersion in the 409 body is the version that was actually committed. Use it to re-fetch the document, merge your changes, and retry the save.
Re-fetch the document at the current version
version field from the YAML frontmatter (e.g. version: 7).Merge your changes with the new content
Inspect the content that was committed between your original read and now. Incorporate your intended changes against the new base.
Rate Limit Errors (429)
Rate limiting is enforced in-memory with a per-window counter. The three guarded surfaces are:- Login — keyed per
(username, IP). A successful login resets the counter for that pair, so honest retries after a typo do not accumulate. - Registration — keyed per IP.
- Anonymous uploads — keyed per IP. Signed-in accounts are not rate-limited on uploads because they are attributable and their household owns the subject resource.
Error Handling in the api() Wrapper
Routes call api(handler) which provides the catch boundary. The mapping is exhaustive for typed errors:
| Thrown class | HTTP status | Extra fields |
|---|---|---|
NotFoundError | 404 | — |
ForbiddenError | 403 | — |
ConflictError | 409 | currentVersion (when present) |
ValidationError | 400 | — |
RateLimitError | 429 | — |
| (anything else) | 500 | (re-thrown, not serialized) |