Skip to main content

Documentation 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.

Hashboard’s service layer uses a set of typed error classes that map directly to HTTP status codes. Every route handler is wrapped by the 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 an error field containing a human-readable message. The Content-Type is always application/json.
{
  "error": "cards not found"
}
The 409 Conflict response on document saves includes an additional field:
{
  "error": "Conflict: document was modified",
  "currentVersion": 7
}

HTTP Status Codes

StatusError ClassWhen it occurs
400ValidationErrorThe 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.
403ForbiddenErrorThe 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.
404NotFoundErrorThe 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.
409ConflictErrorA 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.
429RateLimitErrorToo 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:
  1. Schema validationreadBody(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".
  2. Service-layer checks — semantic constraints that Zod cannot express, such as checking that prevId and nextId belong to the target column on a move operation, or that a ?filename= query parameter was provided for an upload.
{
  "error": "baseVersion: Expected number, received string; content: Required"
}

Conflict Errors and Optimistic Concurrency (409)

Document saves use optimistic concurrency. When you fetch a document — via GET /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.
# Attempt to save a doc at version 5
curl -X PUT https://hashboard.example.com/api/v1/docs/DOC_ID \
  -H 'Authorization: Bearer hb_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"baseVersion":5,"content":"# Updated content"}'
// 409 response — someone else saved between your read and your write
{
  "error": "Conflict: document was modified",
  "currentVersion": 7
}
1

Re-fetch the document at the current version

# The .md rendition includes the version in YAML frontmatter
curl https://hashboard.example.com/docs/DOC_ID.md \
  -H 'Authorization: Bearer hb_TOKEN'
Read the version field from the YAML frontmatter (e.g. version: 7).
2

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.
3

Retry the save with the new baseVersion

curl -X PUT https://hashboard.example.com/api/v1/docs/DOC_ID \
  -H 'Authorization: Bearer hb_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"baseVersion":7,"content":"# Merged content"}'
Same-content saves are no-ops. If your merged content is identical to what is already stored, the PUT returns the current document without incrementing the version.

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": "too many login attempts; try again later"
}
If Hashboard is behind a reverse proxy, rate limiting only works correctly when ADDRESS_HEADER and XFF_DEPTH are configured. Without them, every visitor appears to come from the proxy’s single IP address and they all share one bucket.

Error Handling in the api() Wrapper

Routes call api(handler) which provides the catch boundary. The mapping is exhaustive for typed errors:
Thrown classHTTP statusExtra fields
NotFoundError404
ForbiddenError403
ConflictError409currentVersion (when present)
ValidationError400
RateLimitError429
(anything else)500(re-thrown, not serialized)
Because all expected failures are typed, adding a new error condition means extending the typed error hierarchy — not scattering ad-hoc status codes through individual handlers.

Build docs developers (and LLMs) love