# Vessels — Full Integration Reference > Let your agent reach you. Vessels is the communication layer between AI agents and their human operators. Production URL: https://vessels-two.vercel.app --- ## Setup ### Get an API key ```bash npm install -g vessels # Step 1: send OTP to your email vessels login --email me@example.com # Step 2: verify with the code from the email vessels login --email me@example.com --otp 847293 # Create an API key — printed once, output includes VESSELS_API_KEY=vsl_xxx vessels keys create --name my-project ``` Or sign in at https://vessels-two.vercel.app/auth → Settings → API Keys. ### Install the SDK ```bash npm install vessels-sdk ``` ### Initialise ```ts import { Vessels } from 'vessels-sdk'; const vessels = new Vessels({ apiKey: process.env.VESSELS_API_KEY }); // Debug mode — logs every request and response to console const vessels = new Vessels({ apiKey: process.env.VESSELS_API_KEY, debug: true }); ``` --- ## Naming conventions The SDK and push payload use **camelCase** (JavaScript convention): `vesselTitle`, `vesselStatus`, `pinCard`, `previewUrl`, `externalId`. Webhook payloads use **snake_case** (JSON/HTTP convention): `vessel_id`, `external_id`, `interaction_type`. Poll events are normalised to camelCase by the SDK: `event.vessel.externalId`, `event.interactionType`, `event.messageId`. **Rule of thumb:** anything you write (push, SDK calls) is camelCase. Anything you read from a raw webhook POST body is snake_case. --- ## Send a message ```ts await vessels.push({ vessel: 'booking-123', // string ID — creates vessel if new vesselTitle: 'Sarah Martinez', message: 'New booking received.', // required }); ``` --- ## Interactions (5 types) Attach one interaction per message. Human responds, your agent receives the answer. ### approval ```ts await vessels.push({ vessel: 'booking-123', message: 'Sarah wants to book Saturday 10am.', interaction: vessels.approval({ prompt: 'Confirm this booking?', approveLabel: 'Confirm', rejectLabel: 'Decline', metadata: { out_tray_id: '123', type: 'deposit_invoice' }, // comes back in webhook/poll }), vesselStatus: 'waiting', }); // Response: { action: 'approved' | 'rejected', reason?: string } ``` ### choice Options must be { id, label } objects. ```ts await vessels.push({ vessel: 'booking-123', message: 'Which time should I offer?', interaction: vessels.choice({ prompt: 'Choose a time slot', options: [ { id: 'sat-10am', label: '10am Saturday' }, { id: 'sat-2pm', label: '2pm Saturday' }, ], allowCustom: true, }), }); // Response: { selected: 'sat-10am', customValue: string | null } ``` ### checklist Options must be { id, label } objects. ```ts await vessels.push({ vessel: 'invoice-42', message: 'Which items to include?', interaction: vessels.checklist({ prompt: 'Select line items', options: [ { id: 'session', label: 'Session fee $120', checked: true }, { id: 'travel', label: 'Travel $30' }, ], }), }); // Response: { selected: ['session', 'travel'] } // array of IDs ``` ### text_input ```ts await vessels.push({ vessel: 'lead-88', message: 'Prospect asked about pricing.', interaction: vessels.textInput({ prompt: 'How should I respond?', placeholder: 'Type your reply...', multiline: true, }), }); // Response: { text: 'string' } ``` ### confirm_preview ```ts await vessels.push({ vessel: 'lead-88', message: 'Draft email ready.', interaction: vessels.confirmPreview({ prompt: 'Send this email?', previewUrl: 'https://your-app.com/preview/88', approveLabel: 'Send', rejectLabel: 'Edit', }), }); // Response: { action: 'approved' | 'rejected', reason?: string } ``` --- ## Vessel status ```ts await vessels.push({ vessel: 'booking-123', message: 'Booking confirmed.', vesselStatus: 'resolved', // 'active' | 'waiting' | 'resolved' }); ``` - waiting — amber badge, agent needs human input - active — no badge, normal state - resolved — green badge, entity is done --- ## Pinned card Stays above the message stream, always visible. Replaces previous pinned card. ```ts await vessels.push({ vessel: 'booking-123', message: 'Status updated.', pinCard: { title: 'Booking Status', fields: [ { label: 'Client', value: 'Sarah Martinez' }, { label: 'Status', value: 'Confirmed' }, ], }, }); ``` --- ## Labels Tag vessels for filtering in the dashboard. Set them when pushing or via PATCH. ```ts await vessels.push({ vessel: 'booking-123', message: 'New booking.', labels: ['golf', 'saturday', 'vip'], }); ``` - Max 10 labels per vessel, 50 characters each. - Labels replace the existing set on every push — send all labels you want, not just new ones. - Visible as colour-coded badges in the vessel list. Click to filter by label. - Available in poll events as `event.vessel.labels`. --- ## Get responses — Polling ```ts const { events } = await vessels.poll({ ack: true }); for (const event of events) { if (event.type === 'interaction.response') { // event.interactionType — 'approval' | 'choice' | 'checklist' | 'text_input' | 'confirm_preview' // event.response — see shapes above // event.interactionMetadata — metadata you passed when pushing the interaction (or null) // event.vessel.externalId — your original vessel string ('booking-123') // event.vessel.id — vessel UUID // event.vessel.labels — string[] of tags on this vessel // event.messageId — UUID of the message with the interaction if (event.interactionType === 'approval' && event.response.action === 'approved') { await confirmBooking(event.vessel.externalId); } } if (event.type === 'message.user') { // Human sent a message in the vessel // event.message.content // event.vessel.externalId } } ``` Poll events are camelCase — the SDK normalises the raw API response for you. --- ## Get responses — Webhooks Register via CLI or Settings → Webhooks. You'll receive a webhook secret — store it as `VESSELS_WEBHOOK_SECRET`. Vessels POSTs signed JSON on each event. ```bash vessels webhooks create --url https://myapp.com/hooks/vessels # output includes: VESSELS_WEBHOOK_SECRET= ``` > Note: webhook payloads use snake_case (external_id, interaction_type) — this is the raw HTTP/JSON layer, not the SDK. See the naming conventions section above. ### Webhook payload shape **interaction.response:** ```json { "event": "interaction.response", "vessel_id": "uuid", "workspace_id": "uuid", "data": { "message_id": "uuid", "interaction_type": "approval", "response": { "action": "approved" }, "response_id": "uuid", "metadata": { "out_tray_id": "123" }, "vessel": { "id": "uuid", "external_id": "booking-123", "title": "Sarah Martinez", "metadata": {} } }, "timestamp": "2024-01-01T00:00:00.000Z" } ``` **message.user:** ```json { "event": "message.user", "vessel_id": "uuid", "workspace_id": "uuid", "data": { "message_id": "uuid", "content": "I want to reschedule", "vessel": { "id": "uuid", "external_id": "booking-123", "title": "Sarah Martinez", "metadata": {} }, "context": [{ "source": "agent", "content": "...", "created_at": "..." }] }, "timestamp": "2024-01-01T00:00:00.000Z" } ``` The `context` field contains the last 10 messages in the vessel (oldest first) as a convenience — so your agent has recent history without a separate API call. It is not configurable. This is not the agent's canonical state — treat it as a conversation shortcut, not a source of truth. ### Handler ```ts import { Vessels } from 'vessels-sdk'; import express from 'express'; const vessels = new Vessels({ apiKey: process.env.VESSELS_API_KEY }); const app = express(); app.post('/vessels/webhook', express.raw({ type: '*/*' }), async (req, res) => { const rawBody = req.body.toString(); const valid = await vessels.verifyWebhook( rawBody, req.headers['x-vessels-signature'], process.env.VESSELS_WEBHOOK_SECRET // the secret from Settings → Webhooks ); if (!valid) return res.status(401).send('Invalid signature'); const payload = JSON.parse(rawBody); // payload.event — 'interaction.response' | 'message.user' // payload.vessel_id — vessel UUID // payload.timestamp — ISO timestamp if (payload.event === 'interaction.response') { const { interaction_type, response, vessel } = payload.data; // vessel.external_id — your original vessel string ('booking-123') [snake_case in webhook body] if (interaction_type === 'approval' && response.action === 'approved') { await confirmBooking(vessel.external_id); } } if (payload.event === 'message.user') { const { content, vessel, context } = payload.data; // content — what the human typed // vessel.external_id — your original vessel string // context — last 10 messages, convenience only } res.json({ ok: true }); }); ``` Retries: 3× on failure (500ms, 2s backoff). All deliveries logged in Settings → Logs. --- ## Update vessel without a message For most updates (status, pinned card) you can use `vessels.push()` which handles everything inline. PATCH is for when you want to update state without adding a message to the feed. ### By UUID (from a previous push response) ```ts await fetch(`https://vessels-two.vercel.app/api/v1/vessels/${vesselUuid}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${process.env.VESSELS_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ vesselStatus: 'resolved', pinCard: null }), }); ``` ### By external ID (your own string — no UUID needed) ```ts await fetch('https://vessels-two.vercel.app/api/v1/vessels/by-external/booking-123', { method: 'PATCH', headers: { 'Authorization': `Bearer ${process.env.VESSELS_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ vesselStatus: 'resolved', pinCard: null }), }); ``` Both accept: `pinCard` (null to clear), `vesselStatus`, `title`, `metadata`. --- ## Attachments The developer hosts files. Vessels renders URLs. ```ts await vessels.push({ vessel: 'booking-123', message: 'Receipt processed.', attachments: [ { type: 'image', url: 'https://your-bucket.s3.amazonaws.com/receipt.jpg' }, { type: 'file', url: 'https://your-app.com/reports/q1.pdf', filename: 'Q1 Report.pdf' }, ], }); ``` - Images render inline in the message. Tap to open full size. - Files render as a labelled download link. Tap opens in browser. - Max 10 attachments per message. - No upload endpoint. No file storage. Vessels is just an `` or `` tag pointing at your URL. --- ## Suggested responses Quick reply chips the human can tap to pre-fill their reply. Disappear after any message is sent. ```ts await vessels.push({ vessel: 'booking-123', message: 'Client asked about availability next week.', suggestions: [ 'We have slots on Tuesday and Thursday', "Let me check and get back to you", "We're fully booked next week", ], }); ``` - Tapping fills the text input. The human can edit before sending. - Max 5 suggestions per message. - Not a replacement for interaction cards — suggestions are for free-form replies. --- ## Edit a message (live updates) Update a message's content, card, attachments, or suggestions after it's been sent. The message re-renders in place via Supabase Realtime. Use this for progress bars, live status, or corrections. ```ts const { messageId } = await vessels.push({ vessel: 'batch-job-1', message: 'Processing bookings... 0 of 50 complete', card: { title: 'Batch Progress', fields: [{ label: 'Progress', value: '0 / 50' }] }, }); // Later, as work proceeds: await vessels.editMessage(messageId, { content: 'Processing bookings... 24 of 50 complete', card: { title: 'Batch Progress', fields: [{ label: 'Progress', value: '24 / 50' }] }, }); // When done: await vessels.editMessage(messageId, { content: 'All 50 bookings processed.', card: { title: 'Batch Complete', fields: [{ label: 'Processed', value: '50' }, { label: 'Errors', value: '0' }] }, }); ``` **Rules:** - Only agent-sourced messages can be edited (not user or system messages). - Interaction cards are immutable after creation and cannot be changed via edit. - Updatable fields: `content`, `card`, `attachments`, `suggestions`, `agentActivity`. Raw API: `PATCH /api/v1/messages/:message_id` with `Authorization: Bearer vsl_xxx`. --- ## Agent activity (working state + step history) When your agent works through multiple steps before delivering a result, you can stream those steps into the vessel in real time. On mobile, this renders as an animated working card. When the agent resolves, the card collapses into a small tappable chip — "Agent worked for 23s" — and the human can tap to see every step the agent took. This requires no extra infrastructure. You signal the current phase by setting `agentActivity` on a push or edit. The server accumulates the step history automatically. ### Workflow ```ts // 1. Push to open a working card — no message text needed yet const { messageId } = await vessels.push({ vessel: 'booking-123', agentActivity: { type: 'thinking' }, vesselStatus: 'waiting', }); // 2. Switch phases as your agent progresses — server closes the previous step await vessels.editMessage(messageId, { agentActivity: { type: 'searching', label: 'Checking FlightAware...' }, }); await vessels.editMessage(messageId, { agentActivity: { type: 'tool_use', label: 'Reading your calendar' }, }); // 3. Resolve — null collapses the working card into a chip, message content appears await vessels.editMessage(messageId, { agentActivity: null, content: 'Found 3 flights under $400.', interaction: vessels.choice({ prompt: 'Which works?', options: [...] }), }); ``` ### Activity types Five fixed types — use the one that matches what your agent is doing: | Type | When to use | |------|-------------| | `thinking` | LLM reasoning, planning, deciding | | `searching` | Web search, vector lookup, database query | | `tool_use` | Calling an API, running a function, reading a file | | `browsing` | Reading a webpage, scraping content | | `processing` | Data transform, calculation, batch operation | The optional `label` field adds a plain-text description shown during that step: `{ type: 'tool_use', label: 'Querying your CRM' }`. If omitted, the type name is shown. ### What the human sees **While working:** An animated card appears in the vessel — live BlobSpinner animation, current step label, elapsed timer ticking up. As you switch phases, the animation morphs to match the new activity type. **After resolving:** The working card collapses into a subtle chip above the final message — "Agent worked for 23s" with step-type icons in sequence. Tapping it opens a timeline showing every step, its label, and how long it took. The step history is stored permanently on the message — it doesn't disappear and is visible to anyone who opens the vessel later. ### SDK constants ```ts import { AgentActivityTypes } from 'vessels-sdk'; await vessels.editMessage(messageId, { agentActivity: { type: AgentActivityTypes.toolUse, label: 'Calling Stripe' }, }); ``` `AgentActivityTypes` exports: `thinking`, `searching`, `toolUse` (maps to `'tool_use'`), `browsing`, `processing`. ### Notes - `agentActivity` is optional on `push()`. When set, `message` may be omitted — the push opens a working card with no text content yet. - Each `editMessage` with a new `agentActivity` object closes the previous step (records `ended_at` and `duration_ms` server-side) and starts a new one. - `agentActivity: null` seals the history with `resolved_at`. After this, any further `agentActivity` edits on the same message have no effect. - You can combine `agentActivity: null` with any other edit fields (`content`, `card`, `interaction`, etc.) in the same call. --- ## Broadcast push (pushMany) Push the same message to multiple vessels at once. Each vessel gets its own independent copy — interactions are responded to individually. ```ts const { results } = await vessels.pushMany({ vessels: ['booking-101', 'booking-102', 'booking-103'], message: 'The Windmill Course is closed this Saturday due to maintenance.', interaction: vessels.approval({ prompt: 'Send cancellation email to this client?', approveLabel: 'Send', rejectLabel: 'Skip', }), vesselStatus: 'waiting', }); // results: [{ vessel: 'booking-101', messageId: '...', vesselId: '...' }, ...] ``` - Max 100 vessels per call. - Each vessel is upserted as normal (created if it doesn't exist). - Each interaction is independent — the human approves/rejects per vessel. Raw API: `POST /api/v1/push/many` with `Authorization: Bearer vsl_xxx`. --- ## Full push payload ```ts await vessels.push({ // One of message or agentActivity is required message: 'string', agentActivity: { type: 'thinking' | 'searching' | 'tool_use' | 'browsing' | 'processing', label?: 'string' }, // Vessel vessel: 'string', // external ID, creates if new vesselTitle: 'string', vesselStatus: 'active' | 'waiting' | 'resolved', labels: ['string'], // up to 10 tags, 50 chars each — visible in web dashboard metadata: { key: 'value' }, // passed through in webhooks // Message content card: { title: 'string', fields: [{ label: 'string', value: 'string' }] }, interaction: { ... }, // one of the 5 types pinCard: { title: 'string', fields: [...] }, // null to clear previewUrl: 'string', attachments: [{ type: 'image' | 'file', url: 'string', filename?: 'string' }], suggestions: ['string', ...], // up to 5 quick reply chips }); // Returns: { ok: true, messageId: string, vesselId: string, createdAt: string } ``` --- ## Commands & Mentions Define shorthand your team uses when messaging vessels. Configure them once in Settings → Commands — they apply to all vessels in your workspace. **Commands** (`/`) are actions your agent understands. When a team member types `/` in the message input, an autocomplete dropdown shows all registered commands. **Mentions** (`@`) are routing targets — departments, queues, or anything on your end that receives the forwarded message. Typing `@` shows the registered mentions. Both render with syntax highlighting in the message input and in message history — commands in blue, mentions in purple — so they feel intentional rather than free text. Vessels sends the raw message string to your webhook unchanged. It is your agent's responsibility to parse `/commands` and route `@mentions`: ``` Human types: /accept @billing confirm this one Webhook data.content: "/accept @billing confirm this one" ``` Register commands and mentions at Settings → Commands. Shape: name (e.g. `/accept`, `@billing`) plus an optional description shown in the autocomplete dropdown. --- ## The web dashboard Developers and their team can interact with vessels directly at https://vessels-two.vercel.app — no mobile app required. The web dashboard shows all vessels, their status badges, pinned cards, and full message history. Humans can reply to agent messages and respond to all 5 interaction types directly in the browser. The dashboard updates in real time via Supabase Realtime. This means the web UI is a complete interaction surface on its own — the agent sends, the human responds in the dashboard, the agent polls or receives a webhook. Mobile (Phase B) adds push notifications but is not required for the core loop. --- ## Environment variables ``` VESSELS_API_KEY=vsl_your_key_here VESSELS_WEBHOOK_SECRET=your_webhook_secret # from Settings → Webhooks ``` --- ## CLI reference All commands are non-interactive — safe to run from agents, Claude Code, or CI. ```bash # Auth (two-step OTP — works non-interactively) vessels login --email me@example.com # sends OTP vessels login --email me@example.com --otp # verifies, saves session vessels logout vessels whoami # API keys vessels keys list vessels keys create [--name ] # prints key once, output includes VESSELS_API_KEY=vsl_xxx vessels keys revoke # Webhooks vessels webhooks list vessels webhooks create --url https://myapp.com/hooks/vessels [--events interaction.response,message.user] vessels webhooks delete vessels webhooks enable vessels webhooks disable # Push (agent message, for testing) vessels push --vessel --message --key vsl_xxx # Message (user message — fires webhooks, prints delivery log) vessels message --vessel --message # Accepts vessel UUID or external_id (e.g. booking-123). # Sends a message as the logged-in user, then immediately prints each # webhook delivery that fired: status code, endpoint URL, and response body. # Useful for testing your webhook handler end-to-end from the terminal. ``` ### Example output ``` $ vessels message --vessel booking-123 --message "Rescheduled to 3pm" Sent. vessel_id 4f2a1c… message_id d8b93e… Webhook deliveries (1): ✓ 200 https://myapp.com/hooks/vessels message.user · 2:34:07 PM {"ok":true} ``` If the delivery fails you see the exact status code and response body your endpoint returned — no need to dig through server logs.