npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vessels-sdk

v0.4.1

Published

Let your agent reach you. Official Vessels SDK.

Downloads

1,141

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-sdk

Quick 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 create

API 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, before JSON.parse
  • signature — the X-Vessels-Signature header 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