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

overblast

v0.3.0

Published

Overblast SDK + CLI — manage workspaces, social posts, DMs, webchat, email, phone calls, todos, contact memory, and webhooks.

Readme

overblast

A unified SDK + CLI for the Overblast platform: workspaces, social posts/DMs, webchat, email, phone calls, conversation memory, todos, and webhooks.

Works as a Node.js CLI or as an ESM library you import from your own code.

npm install overblast

Quick start

Use it as a Node.js library

import { Overblast } from 'overblast';

const ob = new Overblast({ apiKey: process.env.OVERBLAST_API_KEY! });

// One-call workspace inventory: every channel this key can speak on.
// Returns connected social accounts (Instagram, WhatsApp, …) +
// outbound email (workspace agent address + SMTP if configured) +
// phone (extension on the shared inbound, shared outbound pool,
// private lines).
const summary = await ob.accounts.summary();
//   summary.connectedAccounts     → social accounts with health
//   summary.email.agentAddresses  → e.g. ["<cid>[email protected]", …]
//   summary.email.smtp            → custom SMTP if set + verified
//   summary.phone.extension       → 5-digit IVR ext on shared line
//   summary.phone.sharedInboundNumbers
//   summary.phone.privateLines    → workspace-owned numbers

// List all conversations across a workspace's social inbox
const { conversations } = await ob.conversations.list(profileSetId);

// Reply to a webchat conversation
await ob.webchat.reply(computerId, {
  conversationId: 'webchat_abc123',
  content: 'Hi! Yes, we are open until 6pm today.',
});

// Print a QR for "Table 18" — scanning opens a webchat with the
// position label bound to the conversation. Tasks created during
// that chat carry the position context (assets near the table,
// pricing items relevant to the spot).
const link = await ob.webchat.createLink({
  computerId,
  label: 'Table 18',
  // Optional: assetIds + pricingItemIds the agent should scope to
});
console.log(link.qrUrl); // PNG you can paste into print signage

// Time-bounded invite: signed URL the worker rejects past expiry.
// No DB column — the signature itself is the expiry mechanism, so
// expired links never reach the chat.
const invite = await ob.webchat.createLink({
  computerId,
  label: 'Delivery — order #4421',
  expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
});
console.log(invite.url);          // signed `?signed=<token>` URL
console.log(invite.unsignedUrl);  // never-expires `?link=<id>` URL

// Place a call
const { call } = await ob.calls.create({
  computerId,
  phoneE164: '+15551234567',
  fromPhoneE164: '+15557654321',
  goal: 'Confirm Tuesday\'s 3pm appointment.',
});

// Trigger a smart-staleness sync of upstream resources into Firestore.
// First call paginates fully; subsequent calls within 30s short-circuit
// as 'fresh'; concurrent callers wait on the in-flight refresh via a
// D1-backed lock. Used by the Flutter app on screen-open.
await ob.refresh.resource('comments');
await ob.refresh.resource('reviews');
await ob.refresh.resource('contacts');
await ob.refresh.resource('posts');

// Subscribe to incoming messages in real time
await ob.webhooks.create(computerId, {
  eventType: 'message.received',
  url: 'https://my-app.example.com/inbound',
});

Use it as a CLI

export OVERBLAST_API_KEY=ob_live_...

# One-call channel inventory — what can this key speak through?
overblast accounts summary

overblast profile-sets list
overblast conv list <profileSetId>
overblast webchat reply <computerId> webchat_abc123 "Yes, we're open until 6pm."
overblast email send <computerId> [email protected] "Re: your enquiry" "Thanks for…" --domain 0-0.chat
overblast call <computerId> +15551234567 "Confirm Tuesday's 3pm" --from +15557654321
overblast webhook create <computerId> --event message.received --url https://my-app.example.com/inbound

# Position QR / chat-link — for ordering or chat tied to a place
overblast webchat link create "Table 18"
overblast webchat link bulk-create "Table {n}" --start 1 --end 24 --group dining-room
overblast webchat link qr <linkId>                       # PNG image URL
overblast webchat link create "Delivery #4421" \
  --expires-at 2026-05-10T22:00:00Z                      # signed expiring URL

# Smart-staleness refresh (worker polls upstream → writes Firestore)
overblast refresh posts | comments | reviews | contacts

overblast --help prints the full command reference.


Authentication

The SDK and CLI both authenticate with a workspace API key. Pass it three ways (in priority order):

  1. --api-key ob_live_... flag (CLI only)
  2. OVERBLAST_API_KEY environment variable
  3. new Overblast({ apiKey: '...' }) constructor (library)

API keys are scoped to a single computer (workspace). Mint, scope, and revoke keys in the Overblast app or the developer console — key management is not available via API key. Inspect the key you're currently using with:

overblast api-keys me

The default base URL is https://brain.deployd.network/v1. (The old /social prefix still works as a permanent alias, but prefer /v1.) Override via --base-url (CLI) or baseUrl (library) when targeting staging/preview.

Scope catalog

Each API key carries a list of scopes the worker checks on every request. Scopes follow category:action[:platform]:

  • * — full access
  • category:* — every action in a category (e.g. kb:*)
  • category:action — exact action (e.g. post:create)
  • category:action:platform — platform-qualified (e.g. post:create:instagram)

| Category | Actions | Covers | |---|---|---| | post | create, read, delete | Social posts (publish/list/unpublish) | | dm | send, read | Direct messages on social platforms | | comment | read, reply | Inbox comments + private replies | | analytics | read | Per-platform analytics | | contacts | read | CRM contacts | | todo | create, read, update, delete | Tasks pipeline (uses skipAiExtraction shortcut) | | task-template | create, read, update, delete | Task templates | | document-template | create, read, update, delete | Document templates | | document | create, read, delete | Generated documents | | asset | create, read, update, delete | Workspace assets (rooms, equipment, vehicles) | | catalog | create, read, update, delete | Pricing catalog (subset of business-data) | | kb | create, read, update, delete | Knowledge-base files in R2 | | business-data | read, update | Full business doc (profile + hours + payments + …) | | branding | read, update | Brand profile (logo, colors, voice) | | team | read | Team member list | | ai-search | query | AutoRAG semantic search across the workspace |

Pick the scopes a key carries when you mint it in the Overblast app or the developer console — per-category checkboxes, per-platform optional.


What you can do

The SDK exposes every conversational channel the platform serves, plus the admin and bookkeeping primitives around them.

| Channel | Read messages | Send / reply | Real-time | Notes | |---|---|---|---|---| | Social DMs (Twitter, IG, FB, etc.) | conversations.list / conversations.messages | conversations.sendDm | message.received webhook | Per profile set | | Webchat (website widget) | webchat.info (settings) | webchat.reply | message.received webhook | Conversation IDs are webchat_<deviceId> | | Email (ticketed threads) | n/a — surfaced via webhooks | email.createThread / email.reply | message.received webhook | Inbound goes to {cid}.agent@{domain} | | Phone calls (Twilio + voice agent) | calls.list / calls.conversation / calls.transcriptPdfUrl / calls.recordingUrl | calls.create | call.started / call.ended webhooks | LiveKit-powered voice agent runs the call | | Comments / reviews | passthrough.call (raw Zernio routes) | same | comment.received webhook | Listed under "raw API" below |

Cross-cutting:

| Capability | Method | |---|---| | Workspace todo list (active by default) | todos.list(cid) | | Conversation-scoped todos | todos.list(cid, { conversationId }) | | Archived/all todos | todos.list(cid, { status: 'all' }) | | Contact memory (markdown) | contacts.memory(profileSetId, contactId) | | Bundled conversation context | context.forConversation(...) | | Real-time event push | webhooks.create / list / delete |


Library reference

Constructor

new Overblast({
  apiKey: string,        // required
  baseUrl?: string,      // default https://brain.deployd.network/v1
})

All methods return parsed JSON. Errors throw OverblastError with status/body for inspection:

import { OverblastError } from 'overblast';

try {
  await ob.calls.create({ … });
} catch (e) {
  if (e instanceof OverblastError && e.status === 402) {
    console.warn('Out of credits:', e.body);
  } else throw e;
}

addons — pricing, allowance, payment links

Each workspace subscription includes 5 connected social accounts and 5 team seats. Beyond that, the account owner buys extras — pooled at the account level, shared across every workspace they own:

| Extra | Price | SKU shape | |---|---|---| | 1 connected account / month | 5€ | extra-account, quantity 1..50 | | 1 team seat / month | 15€ | extra-seat, quantity 1..50 | | AI credit top-up (one-shot) | small / medium / large | credits, pack: 'small' \| 'medium' \| 'large' |

Allowance enforcement: POST /profile-sets/:id/accounts and POST /roles/computers/:cid/team-members return HTTP 402 with { slotType, used, allowance, extraNeeded, upgradeUrl } when the caller is at the cap. Catch with OverblastError and route the user to the upgrade flow.

| Method | Endpoint | Notes | |---|---|---| | addons.allowance() | GET /addons/allowance | One-call snapshot: workspaces, slot pool, AI credit balance | | addons.checkout({ kind, quantity }) (extras) | POST /addons/checkout | Mints a Stripe Checkout URL — price + redirect URLs are server-side only | | addons.checkout({ kind: 'credits', pack }) | POST /addons/checkout | One-shot credits top-up; pack maps to a server price | | credits.balance() | (alias of allowance) | Shortcut: returns { balanceCents, currency } or null | | credits.topUp(pack) | (alias of checkout) | Shortcut: addons.checkout({ kind: 'credits', pack }) |

Security model — DO NOT REGRESS: the SDK signature deliberately excludes amountCents, successUrl, and cancelUrl. Those come from worker env vars only (STRIPE_PRICE_*, BILLING_SUCCESS_URL, BILLING_CANCEL_URL). A compromised SDK consumer cannot lower the price or redirect to an attacker-owned site.

const a = await ob.addons.allowance();
//   a.totalAccountSlots = workspaceCount × 5 + extraAccountSlots
//   a.usedAccountSlots  = currently-connected social accounts (all workspaces)
//   a.totalSeatSlots    = workspaceCount × 5 + extraSeatSlots
//   a.usedSeatSlots     = team members across all workspaces
//   a.aiCredits         = { balanceCents, currency } | null

// Hand the upgrade URL to the user (web/desktop). Never use on mobile
// for digital goods — IAP only on iOS/Android (TOS).
const checkout = await ob.addons.checkout({ kind: 'extra-account', quantity: 2 });
console.log(checkout.url); // https://checkout.stripe.com/c/pay/...

// Top up AI credits with a fixed pack (cents value lives only on the worker)
const topUp = await ob.credits.topUp('medium');
console.log(topUp.url);

IAP — iOS + Android product IDs

Mobile-first buyers go through StoreKit / Play Billing. The worker's receipt verifier (/profile-sets/subscribe) maps these product IDs to account-level slot grants — no manual reconciliation:

| Product | Quantity | Type | |---|---|---| | overblast_extra_account_1_monthly | +1 connected account | Auto-renewable subscription | | overblast_extra_account_2_monthly | +2 | "" | | overblast_extra_account_3_monthly | +3 | "" | | overblast_extra_account_5_monthly | +5 | "" | | overblast_extra_account_10_monthly | +10 | "" | | overblast_extra_seat_1_monthly | +1 team seat | Auto-renewable subscription | | overblast_extra_seat_2_monthly | +2 | "" | | overblast_extra_seat_3_monthly | +3 | "" | | overblast_extra_seat_5_monthly | +5 | "" | | overblast_extra_seat_10_monthly | +10 | "" |

iOS uses these as 11 separate products in two subscription groups (overblast_extra_accounts + overblast_extra_seats) so the user auto-switches tier on upgrade. Android mirrors with two subscription products carrying base plans for each tier.

The 10-year launch promo (overblast_workspace_10yr_deal) is off by default in the app — re-enable via Firebase Remote Config flag ten_year_deal_active = true. Existing buyers keep their +1 lifetime workspace regardless of the flag.

kb / aiSearch — knowledge base + R2 AutoRAG semantic search

The workspace KB is folder-organised files in R2 (menus, FAQs, policies, pricing PDFs). AutoRAG indexes everything and the auto-reply agent uses it for retrieval. Files are also indexed alongside entity mirrors — tasks, assets, contacts, templates — so aiSearch.query returns matches across the whole workspace knowledge graph.

| Method | Endpoint | Scope | |---|---|---| | kb.listFolders(cid) | GET /kb/computers/:cid/folders | kb:read | | kb.createFolder(cid, { slug, name, visibility? }) | POST /kb/computers/:cid/folders | kb:create | | kb.updateFolder(cid, slug, patch) | PATCH /kb/computers/:cid/folders/:slug | kb:update | | kb.deleteFolder(cid, slug) | DELETE /kb/computers/:cid/folders/:slug | kb:delete | | kb.listFiles(cid, slug) | GET /kb/computers/:cid/folders/:slug/files | kb:read | | kb.signUploadUrl(cid, { filename, contentType }) | POST /kb/computers/:cid/staging/signed-url | kb:create | | kb.moveFile(cid, { sourceKey, targetSlug }) | POST /kb/computers/:cid/move | kb:update | | kb.deleteFile(cid, key) | DELETE /kb/computers/:cid/files | kb:delete | | kb.sync(cid) | POST /kb/computers/:cid/sync | kb:update | | kb.signedGetUrl(cid, key) | GET /kb/computers/:cid/signed-get | kb:read | | aiSearch.query(cid, { query, mode?, folderSlug?, maxResults? }) | POST /kb/computers/:cid/search | ai-search:query |

mode: 'ai-search' (default) returns an AI-summarised answer with citations; mode: 'search' returns raw matching chunks (cheaper).

const r = await ob.aiSearch.query(cid, {
  query: 'What are the brunch hours on Sunday?',
});
console.log(r.answer);     // synthesised from menu + hours docs
console.log(r.citations);  // points to the source file chunks

businessData / assets / team / branding — workspace knowledge

One R2 doc holds the workspace's structured data: profile, working hours, pricing catalog, scheduling constraints, payments, branding. Subset endpoints expose individual sections so an API key can hold a narrow scope (branding-only, catalog-only).

| Method | Endpoint | Scope | |---|---|---| | businessData.get(cid) | GET /business-knowledge/computers/:cid | business-data:read | | businessData.update(cid, body) | PUT /business-knowledge/computers/:cid | business-data:update | | businessData.getProfile(cid) | GET /business-knowledge/computers/:cid/profile | business-data:read | | businessData.getHours(cid) | GET /business-knowledge/computers/:cid/hours | business-data:read | | businessData.getBranding(cid) | GET /business-knowledge/computers/:cid/branding | branding:read | | businessData.updateBranding(cid, branding) | PUT /business-knowledge/computers/:cid/branding | branding:update | | businessData.getCatalog(cid) | GET /business-knowledge/computers/:cid/catalog | catalog:read | | businessData.updateCatalog(cid, catalog) | PUT /business-knowledge/computers/:cid/catalog | catalog:update | | assets.list(cid) | GET /business-knowledge/computers/:cid/assets | asset:read | | team.list(cid) | GET /business-knowledge/computers/:cid/team | team:read |

update paths read-modify-write through the Zod-validated writeBusinessData helper — bad shapes return 400 with the exact schema issues so callers can fix and retry.

accounts — connected social accounts + workspace channel inventory

| Method | Endpoint | |---|---| | accounts.list({ profileSetId? }) | GET /profile-sets/:psid (returns connectedAccounts only) | | accounts.summary({ profileSetId?, computerId? }) | parallel-fetches the channel inventory below |

accounts.summary() is the one call that answers "what can this key speak through?" — useful for AI-agent system prompts and admin UIs:

const s = await ob.accounts.summary();
//   s.connectedAccounts          → [ { id, platform, platform_username, … } ]
//   s.email.agentAddresses       → [ "<cid>@agent.0-0.chat", "<cid>@agent.o-0.chat", … ]
//   s.email.smtp                 → { fromEmail, fromName, verified, providerHint } | null
//   s.phone.extension            → "12345" — IVR ext on the shared inbound, or null
//   s.phone.sharedInboundNumbers → [ "+1…", "+44…" ] — agent's caller-id pool
//   s.phone.privateLines         → [ { phoneE164, countryIso, capabilities, status, … } ]

posts / comments / reviews / refresh — local-first social reads

The Flutter app reads these from Firestore (real-time stream); SDK consumers can either subscribe to Firestore directly OR use these HTTP methods for one-shot reads. The refresh module triggers an upstream poll → Firestore write so the stream catches up.

| Method | Endpoint | |---|---| | posts.create(opts, { profileSetId? }) | POST /z/:psid/posts (queued for moderation if key is manual) | | posts.list({ limit?, cursor?, sortBy?, status? }, { profileSetId? }) | GET /z/:psid/posts | | posts.get(postId, { profileSetId? }) | GET /z/:psid/posts/:id | | posts.delete(postId, { profileSetId? }) | DELETE /z/:psid/posts/:id | | posts.unpublish(postId, body?, { profileSetId? }) | POST /z/:psid/posts/:id/unpublish | | comments.inbox({ limit?, cursor? }, { profileSetId? }) | GET /z/:psid/inbox/comments (posts grouped by comment counts) | | comments.forPost(postId, { accountId?, limit?, cursor? }, { profileSetId? }) | GET /z/:psid/inbox/comments/:postId (threaded) | | comments.reply(postId, { accountId?, message, parentCommentId? }, { profileSetId? }) | POST /z/:psid/inbox/comments/:postId | | comments.privateReply(postId, commentId, { accountId?, message }, { profileSetId? }) | POST /z/:psid/inbox/comments/:postId/:cid/private-reply | | comments.delete(postId, { accountId?, commentId }, { profileSetId? }) | DELETE /z/:psid/inbox/comments/:postId | | reviews.inbox({ limit?, cursor? }, { profileSetId? }) | GET /z/:psid/inbox/reviews | | reviews.reply(reviewId, { accountId, message }, { profileSetId? }) | POST /z/:psid/inbox/reviews/:rid/reply | | reviews.deleteReply(reviewId, { accountId }, { profileSetId? }) | DELETE /z/:psid/inbox/reviews/:rid/reply (Google Business only) | | refresh.resource('posts'\|'comments'\|'reviews'\|'contacts', { profileSetId? }) | POST /refresh/:psid/:resource |

refresh.resource returns one of: 'fresh' (recent refresh covered this call), 'in_flight' (another caller mid-refresh — wait for the stream), 'refreshed' (this caller did the work — count is the items synced, mode is 'full' on the first sync or 'incremental' after).

webchat.link — position QR codes / invite links

Workspace-defined position labels (Table 18, Room 5, Order #4421) get their own chat URL + QR code. When a visitor scans, the conversation carries the position context — auto-reply agents reason about it, tasks created during the chat inherit it.

| Method | Endpoint | |---|---| | webchat.createLink({ computerId, label, expiresAt?, sequenceGroup?, assetIds?, pricingItemIds? }) | POST /webchat-links | | webchat.bulkCreateLinks({ computerId, labelTemplate, sequence, sequenceGroup?, assetIds?, pricingItemIds? }) | POST /webchat-links/bulk | | webchat.listLinks(computerId, { sequenceGroup? }) | GET /webchat-links | | webchat.deleteLink(linkId) | DELETE /webchat-links/:id | | webchat.linkUrl(computerId, linkId) | pure helper — never-expires ?link=… URL | | webchat.qrCodeUrl(chatUrl, size?) | pure helper — PNG QR image URL |

Two URL shapes come back from createLink:

  • unsignedUrl…?link=<id>. Never expires; print on permanent signage.
  • url — when expiresAt is supplied, this is the signed form …?signed=<token>. The token carries { id, cid, exp } HMAC-signed with WEBCHAT_LINK_SECRET; the worker rejects scans past expiry without a DB lookup. Without expiresAt, url equals unsignedUrl.

qrUrl / unsignedQrUrl are PNG image URLs you can put straight into <img src=…> or print signage.

conversations — social inbox (DMs across platforms)

| Method | Endpoint | |---|---| | conversations.unread({ computerId?, limit? }) | GET /conversations/unread | | conversations.markRead(conversationId, { computerId? }) | POST /conversations/:id/read | | conversations.list(profileSetId, { limit?, cursor?, archived? }) | GET /z/:psid/conversations | | conversations.messages(profileSetId, conversationId, { limit?, before? }) | GET /z/:psid/conversations/:id/messages | | conversations.sendDm(profileSetId, conversationId, { text, attachmentUrl?, attachmentType?, tmpKey? }) | POST /z/:psid/dm/:id | | conversations.start(profileSetId, body) | POST /z/:psid/conversations | | conversations.update(profileSetId, conversationId, body) | PUT /z/:psid/conversations/:id | | conversations.deleteMessage(profileSetId, conversationId, messageId) | DELETE /z/:psid/dm/:id/messages/:mid | | conversations.editMessage(profileSetId, conversationId, messageId, { text }) | PATCH /z/:psid/dm/:id/messages/:mid |

conversations.unread is cross-channel: it also returns webchat, email, and phone threads — each row's channel field says which surface to fetch and reply on, and profileSetId is pre-resolved for the social ones. Replying resets the unread counter; markRead skips a thread without replying (useful for agents polling the inbox when auto-reply is off).

webchat — live website chat

| Method | Endpoint | |---|---| | webchat.info(computerId) | GET /webchat/:cid/info | | webchat.reply(computerId, { conversationId, content?, attachmentUrl?, attachmentType?, tmpKey? }) | POST /webchat/:cid/reply | | webchat.getSettings(computerId) / webchat.updateSettings(cid, settings) | GET / PUT /webchat/settings/:cid | | webchat.createInviteToken(computerId) | POST /webchat/settings/:cid/token |

conversationId is always webchat_<deviceId>.

email — ticketed email threads

| Method | Endpoint | |---|---| | email.createThread({ computerId?, toAddr, toName?, receivedOnDomain, subject, body, attachments?, tmpKey? }) | POST /email/threads | | email.reply(threadId, { body, attachments?, tmpKey? }) | POST /email/threads/:id/reply | | email.quota(computerId?) | GET /email/quota |

receivedOnDomain is one of 'agent.0-0.chat' | 'agent.o-0.chat' | 'agent.0-o.chat' | 'agent.o-o.chat' — the From address on outbound is {computerId}.agent@{receivedOnDomain}.

Attachment items reference R2-stored objects:

{ key: 'kb://my-folder/spec.pdf', filename: 'spec.pdf', contentType: 'application/pdf' }

Inbound mail is delivered by Cloudflare Email Routing to the agent address and fired as a message.received webhook event — there is no polling read API.

calls — phone calls + voice agent

| Method | Endpoint | |---|---| | calls.create({ computerId?, phoneE164, fromPhoneE164, goal, scheduleAt?, contactName?, taskInstructions?, taskTemplateIds?, voice?, language?, skillId? }) | POST /calls/ | | calls.list({ computerId?, status?, direction?, limit?, offset? }) | GET /calls/ | | calls.listConversations({ computerId?, limit? }) | GET /calls/conversations | | calls.conversation(conversationId) | GET /calls/conversations/:id | | calls.get(callId) | GET /calls/:id | | calls.recordingUrl(callId) | GET /calls/:id/recording-url | | calls.transcriptPdfUrl(callId) | GET /calls/:id/transcript-pdf | | calls.reschedule(callId, scheduleAt) | PATCH /calls/:id/schedule | | calls.setGoal(callId, goal) | PATCH /calls/:id/goal | | calls.cancel(callId) | POST /calls/:id/cancel | | calls.sharedNumbers() | GET /calls/shared-numbers |

Notes:

  • fromPhoneE164 must be one of the workspace's shared-numbers.
  • The platform refuses calls when balance < CALL_MIN_CHARGE_CENTS. Catch OverblastError with status === 402 to surface this.
  • goal is the system prompt the agent runs with. taskInstructions adds free-form "how to do it" guidance separate from the goal.

todos — workspace + per-conversation tasks

| Method | Endpoint | |---|---| | todos.list(computerId, opts) | GET /todos | | todos.create(computerId, opts) | POST /todos | | todos.update(computerId, todoId, patch) | PATCH /todos/:id | | todos.delete(computerId, todoId) | DELETE /todos/:id | | todos.recordPayment(computerId, todoId, { deltaAmount, currency, note?, source? }) | POST /todos/:id/payments |

Task / Todo object — canonical structure

A Todo (Task) has the following shape. Everything except title is optional; the worker auto-fills missing fields from the description via AI when the caller hasn't structured them.

{
  // ── Identity + content ────────────────────────────────────────
  id:          string,              // Firestore doc id
  title:       string,              // human title (required)
  description: string,              // free-text body
  status:      'open' | 'in_progress' | 'blocked' | 'completed' | 'archived' | 'draft',
  priority:    'low' | 'normal' | 'high' | 'urgent',
  tags:        string[],

  // ── Timing ────────────────────────────────────────────────────
  dueAt:       string,              // ISO 8601
  endTime:     string,              // ISO 8601 (mutually exclusive with `duration`)
  duration:    string,              // ISO 8601 ("PT1H30M") — alt to endTime

  // ── Location (start + end) ────────────────────────────────────
  location:           { name?, address?, lat?, lng? },
  endLocation:        { address?, lat?, lng? },

  // ── Assignees + groups ────────────────────────────────────────
  assignedToId:       string,       // single user uid shortcut
  assignedTo:         string,       // display name
  groupId:            string,       // workspace group (Sales, Support, …)

  // ── Asset reservations ────────────────────────────────────────
  assignedAssets:     [ { assetId, assetName, units } ],

  // ── Pricing snapshot (computed from `catalog` at create time) ─
  taskPricing: {
    items: [ { catalogItemId, quantity?, snapshot? } ],
    subtotal:  Money,        // { amount: smallest-unit, currency: 'eur' }
    tax:       Money,
    total:     Money,
    paymentStatus: 'unpaid' | 'partial' | 'paid' | 'refunded',
    paidAmount: Money,
  },
  paymentDeadlineAt: string,        // ISO 8601 — auto-cancel if unpaid past this
  paymentUpfrontPercent: number,

  // ── Conversation/contact link (when task came from a chat) ────
  conversationId:    string,
  contactId:         string,
  contactName:       string,
  contactPhone:      string,
  contactEmail:      string,
  platform:          string,        // 'whatsapp' | 'instagram' | 'webchat' | …
  profileSetId:      string,
  relativePosition:  string,        // webchat-only: "Table 18"

  // ── Template lineage (when created from a task template) ──────
  templateId:        string,        // active template version id
  templateData:      Record<string, unknown>,  // user-supplied fields against the template schema
  templateSnapshot:  TemplateSnapshot,         // frozen copy of the template at create time

  // ── Recurrence (when task spawns repeating instances) ─────────
  recurrence: {
    freq:      'daily' | 'weekly' | 'monthly' | 'yearly',
    interval:  number,              // e.g. every 2 weeks
    byWeekday: number[],            // [0..6] Mon..Sun
    byMonthDay: number[],           // [1..31]
    byMonth:   number[],            // [1..12]
    endAt:     string,              // ISO 8601 stop
    count:     number,              // stop after N occurrences
  },
  recurrenceSeriesId: string,       // shared across every instance in the series

  // ── Activity timeline (per-task subcollection, not on the doc itself)
  // Lives at: computers/{cid}/todos/{todoId}/activity/{eventId}
  // Events: 'created' | 'updated' | 'status_changed' | 'payment' | 'comment' | 'completed'

  createdAt:  string,               // ISO 8601
  updatedAt:  string,               // ISO 8601
  createdBy:  string,               // 'api-key' | 'auto-reply' | uid | 'user'
}

Structured-data shortcut: Pass a non-empty title and the worker skips the AI reconstruct step entirely — your fields win unchanged. This is the right path when an agent has already structured the task (saves an LLM round-trip and ~600ms). Pass skipAiExtraction: true to suppress reconstruct even when title is empty (rare).

// Freeform — agent fills in the gaps
await ob.todos.create(cid, {
  title: 'Call John about delivery',
  description: 'He wanted to reschedule the Tuesday 3pm slot to Thursday.',
});

// Structured (another AI built this) — skip the second AI pass
await ob.todos.create(cid, {
  title:       'Table 18 — 2× burger, 1× coke',
  description: 'Order received via webchat.',
  contactId:   'contact_xyz',
  contactName: 'Maria',
  conversationId: 'webchat_abc',
  platform:    'webchat',
  taskPricing: {
    items: [
      { catalogItemId: 'cat_burger', quantity: 2 },
      { catalogItemId: 'cat_coke',   quantity: 1 },
    ],
  },
  // skipAiExtraction: true,   // ← only needed if `title` were empty
});

todos.list defaults to active only (status: 'open'). Useful filters:

// All workspace open todos
await ob.todos.list(cid);

// Including archived/completed
await ob.todos.list(cid, { status: 'all' });

// Open todos linked to a specific conversation
await ob.todos.list(cid, { conversationId });

// All todos (incl. archived) for a contact across threads
await ob.todos.list(cid, { contactId, status: 'all' });

contacts — directory + per-contact memory

| Method | Endpoint | |---|---| | contacts.list(profileSetId, { limit?, page?, q? }) | GET /z/:psid/contacts | | contacts.memory(profileSetId, contactId) | GET /z/:psid/contacts/:cid/memory |

contacts.memory returns { content: string } — a markdown document the auto-reply agent maintains about the contact (preferences, prior asks, etc.).

context — bundled conversation context

const ctx = await ob.context.forConversation({
  computerId,
  conversationId,
  contactId,           // optional — needed for memory + cross-thread todos
  profileSetId,        // optional — needed for memory + recent messages
  kind: 'social',      // 'social' | 'webchat' | 'call' | 'email'
  includeArchived: false,
});
// → { conversationId, contactId, memory, todos, recentMessages?, recentCalls? }

Convenience wrapper that parallel-fetches contact memory + active todos + the most recent messages or calls. Use this to give an LLM a single payload to reason about a thread.

webhooks — real-time event push

| Method | Endpoint | |---|---| | webhooks.list(computerId) | GET /webhooks/computers/:cid | | webhooks.create(computerId, { eventType, url }) | POST /webhooks/computers/:cid | | webhooks.delete(computerId, webhookId) | DELETE /webhooks/computers/:cid/:id |

Supported event types:

| Event | When it fires | Key payload fields | |---|---|---| | message.received | Inbound DM, webchat message, or email | conversationId, platform, content, contactId, contactName | | comment.received | New comment on a published post | postId, commentId, commentText, authorName | | dm.sent | Outbound DM completes upstream | conversationId, response (raw upstream body) | | post.published | Scheduled post publishes | postId, platforms, content | | call.started / call.ended | Call lifecycle | callId, direction, phoneE164, summary?, disposition? | | todo.{created,updated,deleted} | Todo lifecycle | todoId, title, status | | task_template.{created,updated,deleted} | Template CRUD | templateId, fields |

Use eventType: '*' to subscribe to everything.

The worker signs payloads with the webhook_secret returned at creation time; verify by HMAC-SHA256 over the raw body before trusting the event.

posts / dm / profileSets / addons / apiKeys

The original surface from version 0.1.0 is preserved unchanged:

ob.profileSets.{ list, get, create, update, delete, connectAccount, disconnectAccount }
ob.posts.{ create, raw }
ob.dm.send                    // legacy one-shot DM (prefer conversations.sendDm)
ob.addons.{ list, get, create, cancel }
ob.apiKeys.{ list, create, revoke }
ob.passthrough.call           // raw social-API passthrough by computer id

CLI reference

Every library method has a CLI command. Run overblast --help for the authoritative list. Highlights:

# Conversations & messages
overblast conv list <profileSetId>
overblast conv messages <profileSetId> <convId> --limit 100
overblast conv send <profileSetId> <convId> "Got it — see you Tuesday."

# Webchat
overblast webchat reply <computerId> webchat_abc123 "We open at 9am."
overblast webchat token <computerId>

# Email
overblast email quota --computer <cid>
overblast email send <cid> [email protected] "Quote follow-up" "Hi Jane…" --domain 0-0.chat
overblast email reply <threadId> "Quick clarification on item 2…"

# Calls
overblast call <cid> +15551234567 "Confirm Tuesday's 3pm" --from +15557654321
overblast call list --computer <cid> --status completed
overblast call recording <callId>
overblast call transcript <callId>

# Todos
overblast todos list <cid>                                  # active only
overblast todos list <cid> --status all                     # incl. archived
overblast todos list <cid> --conversation <convId>          # per-conversation
overblast todos create <cid> --title "Send invoice" --conversation <convId>

# Conversation context (memory + todos + recent activity)
overblast context <cid> <convId> --kind social --profile-set <psid> --contact <contactId>

# Webhooks
overblast webhook list <cid>
overblast webhook create <cid> --event message.received --url https://app.example.com/in
overblast webhook delete <cid> <webhookId>

Error handling

Every method throws OverblastError on non-2xx:

catch (e) {
  if (e instanceof OverblastError) {
    e.status   // HTTP status
    e.message  // server-provided "error" string
    e.body     // full parsed response body (for codes/conflicts/etc.)
  }
}

Common cases worth special-casing:

| Status | Meaning | Typical body | |---|---|---| | 400 | Bad input (e.g. missing computerId, malformed E.164) | { error } | | 401 | Bad / missing API key | { error } | | 402 | Out of credits (calls + media gen) | { error, balanceCents, minRequiredCents } | | 403 | Subscription not active for the workspace | { error } | | 422 | Destination blocked / too expensive (calls) | { error, reason, twilioUsdPerMin } | | 429 | Daily email limit exceeded | { error, threadId, ticket } | | 503 | Backend not configured | { error } |


Wiring it into Claude Code (or any AI agent)

The natural recipe for an agent that needs to talk to external people:

  1. Subscribe to message.received (and call.ended if calling) via webhooks.create. Verify the signature, hand the payload to the agent.
  2. Read context with context.forConversation(...) so the agent has contact memory + open todos + recent messages.
  3. Reply through the right channel:
    • conversations.sendDm for social DMs
    • webchat.reply for website chat
    • email.reply (or email.createThread to start one)
    • calls.create to place a call
  4. Track follow-up with todos.create({ ..., conversationId }) so the work survives across conversations.

A minimal Node webhook receiver:

import { Overblast } from 'overblast';
import express from 'express';

const ob = new Overblast({ apiKey: process.env.OVERBLAST_API_KEY! });
const app = express();

app.post('/inbound', express.json(), async (req, res) => {
  const { event, payload } = req.body;
  if (event !== 'message.received') return res.json({ ok: true });

  const { conversationId, platform, content, contactId, contactName } = payload;

  const ctx = await ob.context.forConversation({
    computerId: payload.computerId,
    conversationId,
    contactId,
    profileSetId: payload.profileSetId,
    kind: platform === 'website' ? 'webchat' : 'social',
  });

  const reply = await yourAgent({ ctx, incoming: content, contactName });

  if (platform === 'website') {
    await ob.webchat.reply(payload.computerId, { conversationId, content: reply });
  } else {
    await ob.conversations.sendDm(payload.profileSetId, conversationId, { text: reply });
  }
  res.json({ ok: true });
});

app.listen(8080);

Claude Code skill

The package ships a self-contained skill at skill/SKILL.md. Install it into your Claude Code config so the agent picks the right tool for the task:

overblast install-skill                  # → ~/.claude/skills/overblast/SKILL.md
overblast install-skill --project        # → ./.claude/skills/overblast/SKILL.md
overblast install-skill --dest <path>    # → <path>/overblast/SKILL.md
overblast install-skill --name custom-name   # rename the skill folder
overblast install-skill --force          # overwrite existing

The skill file teaches Claude when to reach for the SDK/CLI, lists the decision matrix (which method handles which conversational channel), and encodes the platform's hard rules (E.164 only, no PII in prompts, etc.).

If you publish a derivative package, you can also vendor skill/SKILL.md directly into your repo — it has no runtime dependencies.

Building from source

cd packages/overblast
npm install
npm run build       # tsup → dist/{index,cli}.{js,d.ts}
npm run dev         # watch mode
npm run typecheck   # tsc --noEmit

The package ships ESM only. The compiled CLI is dist/cli.js and the bin entry overblast points at it.

Testing

Tests hit the real Overblast backend — no mocks. A flag prevents real side-effecting operations (placing actual calls, sending real emails, creating webhooks) from running unless you opt in.

# Read-only suite — never sends a real message, places a real call, etc.
OVERBLAST_API_KEY=ob_test_... npm test

# Full suite — opts into side-effecting tests. Each side-effecting op is
# individually gated on the env vars it needs.
OVERBLAST_API_KEY=ob_test_... \
  OVERBLAST_TEST_ALLOW_SIDE_EFFECTS=1 \
  OVERBLAST_TEST_FROM_PHONE=+15551110000 \
  OVERBLAST_TEST_TO_PHONE=+15551112222 \
  [email protected] \
  OVERBLAST_TEST_WEBHOOK_URL=https://webhook.site/your-uuid \
  npm test

Without OVERBLAST_API_KEY, the suite skips itself cleanly so CI without secrets stays green.

Recognized test env vars:

| Variable | Effect | |---|---| | OVERBLAST_API_KEY | Required to run any test. | | OVERBLAST_BASE_URL | Override base URL (default: production). | | OVERBLAST_TEST_COMPUTER_ID | Workspace under test (auto-detected if omitted). | | OVERBLAST_TEST_PROFILE_SET_ID | Profile set used by conversation/contact tests (auto-detected if omitted). | | OVERBLAST_TEST_ALLOW_SIDE_EFFECTS=1 | Master switch for write tests. Default OFF. | | OVERBLAST_TEST_FROM_PHONE / OVERBLAST_TEST_TO_PHONE | Caller-id + destination for the call test. | | OVERBLAST_TEST_TO_EMAIL / OVERBLAST_TEST_DOMAIN | Destination + agent domain for the email test. | | OVERBLAST_TEST_WEBHOOK_URL | HTTPS endpoint for the webhook round-trip test. |

CI

.github/workflows/overblast.yml runs build + typecheck on every push and PR (no creds needed), and the read-only test suite when the OVERBLAST_API_KEY secret is configured. Side-effecting tests do not run in CI — they require an explicit OVERBLAST_TEST_ALLOW_SIDE_EFFECTS=1 which the workflow does not set.

License

MIT.