Open appvitae.ai

Response Format

New Vitae.ai endpoints should use explicit JSON:API serializers at the controller boundary. The goal is that every API response is predictable, regardless of whether the frontend, CLI, MCP server, or a customer integration consumes it.

Serializers must live in packages/serializers, not inline in controllers or API modules. Controllers should call serializeSingle(request, Serializer, doc) or serializeCollection(request, Serializer, paginatedResult).

Single resource

{
  "data": {
    "id": "client_01h...",
    "type": "client",
    "attributes": {
      "name": "Acme Corp",
      "createdAt": "2026-05-15T13:00:00.000Z"
    }
  },
  "links": {
    "self": "/v1/clients/client_01h..."
  }
}

Resource list

{
  "data": [
    {
      "id": "job_01h...",
      "type": "job",
      "attributes": {
        "title": "Senior Backend Engineer"
      }
    }
  ],
  "links": {
    "self": "/v1/jobs?page=1&limit=25",
    "pagination": {
      "page": 1,
      "limit": 25,
      "total": 125,
      "totalPages": 5,
      "hasNextPage": true,
      "hasPreviousPage": false
    }
  }
}

Action result

Workflow commands still return a resource envelope. The changed resource is returned in data; secondary entities can be included in meta.

{
  "data": {
    "id": "inbox_item_01h...",
    "type": "inbox_item",
    "attributes": {
      "status": "accepted"
    }
  }
}

Errors

{
  "error": {
    "code": "validation_error",
    "message": "Validation failed",
    "details": [
      { "field": "email", "message": "must be a valid email" }
    ],
    "requestId": "req_01h..."
  }
}

Serializer rules

  • Every resource exposes id as a string and type as a stable resource type.
  • Public fields live under attributes.
  • Relationships live under relationships when serialized.
  • Internal database fields like _id, __v, raw ObjectIds, and deleted flags are not returned unless explicitly part of the public contract.
  • Dates are ISO 8601 strings.
  • Field names are camelCase.
  • Lists are always arrays under data.
  • Pagination lives under links.pagination.
  • Errors always use the error envelope.

Migration note

The API has a global compatibility serializer that normalizes normal controller returns into { data, meta } while endpoints are migrated. JSON:API documents returned from explicit serializers bypass that compatibility wrapper.

Endpoints that manually write to the Express response object, stream files, or intentionally opt out with @SkipResponseSerialization() are compatibility surfaces and must document their custom response format.

Current manual-response surfaces:

  • Stripe webhooks
  • CSV, XLSX, attachment, and QR-code downloads
  • OAuth redirects and revocation callbacks
  • short URL redirects
  • streaming agent chat
  • candidate search streaming/export responses