vessels-sdk
v0.4.1
Published
Let your agent reach you. Official Vessels SDK.
Downloads
1,141
Maintainers
Readme
vessels-sdk
Node.js SDK for Vessels — let your agent reach you.
Vessels is the communication layer between AI agents and their human operators. Your agent pushes structured messages to a vessel; the human responds via the web or mobile app; your agent receives the answer via polling or webhooks.
Installation
npm install vessels-sdkQuick start
import { Vessels } from 'vessels-sdk';
const vessels = new Vessels({ apiKey: process.env.VESSELS_API_KEY! });
const { messageId, vesselId } = await vessels.push({
vessel: 'booking-123',
vesselTitle: 'Sarah Martinez — Saturday Booking',
message: 'New booking request received.',
card: {
title: 'Booking Details',
fields: [
{ label: 'Date', value: 'Saturday 14 June' },
{ label: 'Time', value: '2:00 PM' },
{ label: 'Players', value: '4' },
],
},
interaction: vessels.approval({
prompt: 'Confirm this booking?',
approveLabel: 'Confirm',
rejectLabel: 'Decline',
}),
vesselStatus: 'waiting',
});Get your API key from Settings → API Keys in the Vessels app, or via the CLI:
npm install -g vessels
vessels login
vessels keys createAPI reference
new Vessels(config)
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| apiKey | string | required | Your vsl_ prefixed API key |
| baseUrl | string | https://vessels.app | Override for local dev or self-hosted |
vessels.push(payload)
Push a message to a vessel. Creates the vessel if it doesn't exist yet.
Returns { ok: true, messageId: string, vesselId: string, createdAt: string }.
Full payload:
| Field | Type | Description |
|-------|------|-------------|
| message | string | Required. The message text. |
| vessel | string | Your external ID for this vessel (e.g. booking-123). Creates the vessel on first use. |
| vesselTitle | string | Human-readable name shown in the vessel list. Set on creation; updates on subsequent pushes. |
| vesselStatus | 'active' \| 'waiting' \| 'resolved' | Status badge shown in the vessel list. waiting = amber, resolved = green. |
| labels | string[] | Tags for filtering in the dashboard. Max 10, 50 chars each. Replaces the existing set on every push — send all labels you want, not just new ones. |
| metadata | object | Arbitrary JSON stored on the vessel, passed through in webhook callbacks. |
| card | Card | Structured key-value info attached to this message. { title: string, fields: [{ label, value }] } |
| interaction | Interaction | Interactive prompt for the human (one of 5 types — see helpers below). Max one per message; immutable after the human responds. |
| pinCard | Card \| null | Persistent card pinned to the vessel header. Always visible above the message stream. Replaces any existing pinned card. Pass null to clear. |
| attachments | Attachment[] | Images or files to show in the message. Max 10. You host the files; Vessels renders them. [{ type: 'image' \| 'file', url: string, filename?: string }] |
| suggestions | string[] | Quick-reply chips shown below the message. Max 5. Tapping fills the text input. Disappear after the user sends any message. |
| previewUrl | string | URL used by confirm_preview interactions. |
| notify | boolean | Whether to send a push notification. Default true. |
await vessels.push({
vessel: 'booking-123',
vesselTitle: 'Sarah Martinez',
message: 'Ready for review.',
vesselStatus: 'waiting',
labels: ['golf', 'vip'],
metadata: { bookingRef: 'BK-001' },
card: {
title: 'Booking',
fields: [{ label: 'Date', value: 'Saturday 14 June' }],
},
pinCard: {
title: 'Current Status',
fields: [{ label: 'Status', value: 'Awaiting confirmation' }],
},
attachments: [
{ type: 'image', url: 'https://example.com/photo.jpg' },
{ type: 'file', url: 'https://example.com/report.pdf', filename: 'Report.pdf' },
],
suggestions: ['Looks good', 'Need changes'],
interaction: vessels.approval({ prompt: 'Approve?' }),
});vessels.pushMany(payload)
Push the same message to multiple vessels at once. Max 100 vessels per call. Each vessel gets its own independent copy — interactions are responded to individually.
Returns { ok: true, results: Array<{ vessel, messageId, vesselId, error? }> }.
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',
});
for (const r of results) {
if (r.error) console.error(`Failed for ${r.vessel}:`, r.error);
else console.log(`Pushed to ${r.vessel} — message ${r.messageId}`);
}pushMany accepts the same fields as push, except vessel and vesselTitle are replaced by vessels (an array of external IDs). Vessel titles are not settable via pushMany — use per-vessel push calls for that.
vessels.editMessage(messageId, patch)
Edit an existing agent-sent message in place. The message re-renders via Supabase Realtime without reloading.
Updatable fields: content, card, attachments, suggestions. Interactions are immutable — create a new message to re-ask.
Only agent-sourced messages can be edited. Returns { ok: true }.
const { messageId } = await vessels.push({
vessel: 'batch-job-1',
message: 'Processing bookings... 0 of 50 complete',
card: { title: 'Progress', fields: [{ label: 'Progress', value: '0 / 50' }] },
});
// Update as work proceeds:
await vessels.editMessage(messageId, {
content: 'Processing bookings... 24 of 50 complete',
card: { title: 'Progress', fields: [{ label: 'Progress', value: '24 / 50' }] },
});
// Final update:
await vessels.editMessage(messageId, {
content: 'All 50 bookings processed.',
card: {
title: 'Batch Complete',
fields: [{ label: 'Processed', value: '50' }, { label: 'Errors', value: '0' }],
},
});Interaction helpers
All five helpers return a typed interaction object to pass as payload.interaction. There is a fixed set of five types — no custom types.
vessels.approval(opts)
A yes/no decision. Optionally require a reason on rejection.
vessels.approval({
prompt: 'Send the invoice?',
approveLabel: 'Send', // default: 'Approve'
rejectLabel: 'Cancel', // default: 'Reject'
reasonRequired: false, // if true, rejection requires a reason
metadata: { invoiceId: '42' }, // returned in poll event and webhook
})Response shape: { action: 'approved' | 'rejected', reason?: string }
vessels.choice(opts)
Pick one option from a list. Optionally allow a free-text custom value.
vessels.choice({
prompt: 'Which time slot works?',
options: [
{ id: '9am', label: '9:00 AM' },
{ id: '2pm', label: '2:00 PM' },
{ id: '5pm', label: '5:00 PM' },
],
allowCustom: true,
customPlaceholder: 'Type a different time...',
})Response shape: { selected: string, customValue?: string | null }
vessels.checklist(opts)
Pick one or more items from a list.
vessels.checklist({
prompt: 'Which documents are needed?',
options: [
{ id: 'id', label: 'Photo ID', checked: true },
{ id: 'proof', label: 'Proof of address' },
{ id: 'contract', label: 'Signed contract' },
],
minSelections: 1,
submitLabel: 'Confirm selection',
})Response shape: { selected: string[] } — array of selected option IDs.
vessels.textInput(opts)
Free-form text from the human.
vessels.textInput({
prompt: 'Why is the booking being cancelled?',
placeholder: 'Enter reason...',
multiline: true,
submitLabel: 'Submit',
})Response shape: { text: string }
vessels.confirmPreview(opts)
Approve or reject after reviewing an external preview (draft email, document, image).
vessels.confirmPreview({
prompt: 'Review the draft email before sending.',
previewUrl: 'https://your-app.com/drafts/123',
previewLabel: 'View draft',
approveLabel: 'Send',
rejectLabel: 'Edit',
reasonRequiredOnReject: true,
})Response shape: { action: 'approved' | 'rejected', reason?: string }
vessels.poll(options?)
Fetch pending events. Uses a server-side cursor per API key — each call returns only new events since the last acknowledged poll.
Returns { ok: true, events: PollEvent[], hasMore: boolean }.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| ack | boolean | true | Advance the cursor so these events aren't returned again |
| limit | number | 50 | Max events per call |
| since | string | — | ISO timestamp — overrides the cursor for this call only |
Poll events are normalised to camelCase by the SDK.
const { events, hasMore } = 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 — response shape depends on interactionType (see above)
// event.interactionMetadata — metadata object you passed when creating the interaction, or null
// event.messageId — UUID of the message containing the interaction
// event.vessel.externalId — your original vessel string ('booking-123')
// event.vessel.id — vessel UUID
// event.vessel.labels — string[] of tags on this vessel
// event.user — { id, email } of the responding user, or null
if (event.interactionType === 'approval' && event.response.action === 'approved') {
await confirmBooking(event.vessel.externalId);
}
}
if (event.type === 'message.user') {
// event.message.content — what the human typed
// event.vessel.externalId — your original vessel string
const reply = await generateReply(event.message.content);
await vessels.push({ vessel: event.vessel.externalId, message: reply });
}
}
// For high volume, page through all pending events
if (hasMore) {
// Call poll() again immediately
}Polling is a complete alternative to webhooks. Use it to get started, or when you can't receive inbound HTTP.
vessels.verifyWebhook(body, signature, webhookSecret)
Verify the X-Vessels-Signature header on incoming webhook requests. Uses constant-time HMAC comparison.
body— raw request body as a string, beforeJSON.parsesignature— theX-Vessels-Signatureheader value (format:sha256=<hex>)webhookSecret— the per-endpoint secret shown when you created the webhook in Settings → Webhooks. Not the same as your API key.
Returns true if the signature is valid, false otherwise.
import express from 'express';
import { Vessels } from 'vessels-sdk';
const vessels = new Vessels({ apiKey: process.env.VESSELS_API_KEY! });
const app = express();
app.post(
'/webhooks/vessels',
express.raw({ type: 'application/json' }),
async (req, res) => {
const body = req.body.toString('utf8');
const signature = req.headers['x-vessels-signature'] as string;
const valid = vessels.verifyWebhook(body, signature, process.env.VESSELS_WEBHOOK_SECRET!);
if (!valid) return res.status(401).json({ error: 'Invalid signature' });
const payload = JSON.parse(body);
// payload.event — 'interaction.response' | 'message.user'
// Webhook bodies use snake_case (vessel_id, external_id, interaction_type)
if (payload.event === 'interaction.response') {
const { interaction_type, response, vessel, metadata } = payload.data;
// vessel.external_id — your original vessel string
if (interaction_type === 'approval' && response.action === 'approved') {
await confirmBooking(vessel.external_id);
}
}
if (payload.event === 'message.user') {
const { content, vessel, context } = payload.data;
// context — last 10 messages in the vessel, oldest first (convenience, not canonical state)
const reply = await generateReply(content, context);
await vessels.push({ vessel: vessel.external_id, message: reply });
}
res.json({ ok: true });
},
);Webhook payload shapes (snake_case — raw HTTP/JSON layer):
// interaction.response
{
"event": "interaction.response",
"vessel_id": "uuid",
"workspace_id": "uuid",
"data": {
"message_id": "uuid",
"interaction_type": "approval",
"response": { "action": "approved" },
"response_id": "uuid",
"metadata": { "invoiceId": "42" },
"vessel": { "id": "uuid", "external_id": "booking-123", "title": "Sarah Martinez", "metadata": {} }
},
"timestamp": "2024-01-01T00:00:00.000Z"
}
// message.user
{
"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"
}Retries: 3× on failure with backoff (1s, 10s, 60s). All deliveries logged in Settings → Logs.
Error handling
Three typed error classes are exported alongside Vessels:
| Class | HTTP status | Description |
|-------|-------------|-------------|
| VesselsAuthError | 401 | Invalid or revoked API key |
| VesselsValidationError | 400 | Bad request payload. Check .details for field-level errors. |
| VesselsRateLimitError | 429 | Rate limit exceeded. Check .retryAfter (seconds) before retrying. |
import { Vessels, VesselsAuthError, VesselsRateLimitError, VesselsValidationError } from 'vessels-sdk';
try {
await vessels.push({ vessel: 'booking-123', message: 'hello' });
} catch (err) {
if (err instanceof VesselsAuthError) {
console.error('Check your API key — it may be revoked');
} else if (err instanceof VesselsRateLimitError) {
console.error(`Rate limited — retry after ${err.retryAfter}s`);
} else if (err instanceof VesselsValidationError) {
console.error('Bad payload:', err.details);
} else {
throw err;
}
}Naming conventions
The SDK and push payload use camelCase (JavaScript convention): vesselTitle, vesselStatus, pinCard, vesselId, messageId.
Webhook POST bodies use snake_case (raw HTTP/JSON layer): 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 calls, SDK methods) is camelCase. Anything you read from a raw webhook POST body is snake_case.
TypeScript types
All types used by the SDK are exported directly from vessels-sdk:
import type {
PushPayload,
PushManyPayload,
MessagePatch,
Attachment,
Interaction,
Card,
ApprovalInteraction,
ChoiceInteraction,
ChecklistInteraction,
TextInputInteraction,
ConfirmPreviewInteraction,
} from 'vessels-sdk';Links
- Dashboard: https://vessels.app
- Full integration reference: https://vessels.app/docs
- CLI:
npm install -g vessels - npm: https://www.npmjs.com/package/vessels-sdk
