Headless is the most powerful and most expensive layer: reach for it only when no other layer fits.
The model
Three kinds of hooks (full list grouped inHooks):
- Read (data/state):
useCommentAnnotations,useCommentAnnotationById,useUnreadCommentAnnotationCountOnCurrentDocument,useCommentModeState, … → reactive data you render. - Mutate (actions):
useAddCommentAnnotation,useAddComment,useUpdateComment,useDeleteComment,useDeleteCommentAnnotation,useResolveCommentAnnotation,useUpdateStatus,useToggleReaction,useGetLink, … → call these from your own buttons. - Control:
useVeltClient,useSetDocuments,useIdentify,useVeltInitState, … → init/scope/imperative control.
Steps
Step 1: Render data from a read hook
useCommentAnnotations() is reactive: it re-renders when comments change anywhere (including other users in real time). You never poll.
Step 2: Wire your buttons to action hooks
Your components are fully interactive (unlike wireframes). Call the action hooks from your own handlers:Step 3: Build objects to the SDK data model
Mutations expect objects shaped like Velt’s types (from@veltdev/types). You construct them yourself. Example Comment builder:
Because you hand-build these, read the type (MinimalComment,CommentAnnotation,Userin@veltdev/types) and fill every field the action needs. Missing fields are the most common headless bug.
Comment for addComment / updateComment (from the Comment type): commentId (number: auto if omitted), type ('text' | 'voice', default 'text'), from (a full User: required), commentText, commentHtml, status ('added' | 'updated'), and the array fields you touch (attachments, taggedUserContacts, reactionAnnotationIds). from is the field most often missed.
What each action expects (the request object)
Action hooks take a single request object, not loose args. The required fields per common mutation (optional ones omitted; all also acceptoptions?):
Exact, current shapes are the*Requestinterfaces in@veltdev/types(AddCommentRequest,UpdateStatusRequest, …): read the type if a call errors. The table is the field-level companion to R13 (“fill every field the action needs”).
What it can and can’t do
Notes & deep-dives
The cost (be honest about this)
Going headless means you re-implement and maintain everything Velt’s UI gave you for free:- Thread layout, replies, collapsing, edit-in-place, resolved state.
- The composer: @mentions UI, attachments, reactions, formatting.
- Filtering/sorting (compute client-side over
useCommentAnnotations()). - Empty/loading states, accessibility, dark mode.
- Upgrade burden: new Velt features won’t appear in your UI automatically.
When headless is the right call
- Comments rendered onto a non-DOM surface (PDF, canvas, a video timeline) where Velt can’t draw.
- A panel built entirely from your own interactive design-system components.
- Behavior that no slot offers (custom inline workflows, bespoke filtering UIs): drive Velt via hooks + the client API.
Checklist
- Using real hook names from
Hooks. - Objects passed to mutations match
@veltdev/types(all required fields). - Reactive data comes from read hooks (no manual polling).
- You’ve confirmed a wireframe genuinely can’t do it (headless is the costly last resort).

