overblast
v0.3.0
Published
Overblast SDK + CLI — manage workspaces, social posts, DMs, webchat, email, phone calls, todos, contact memory, and webhooks.
Maintainers
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 overblastQuick 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 | contactsoverblast --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):
--api-key ob_live_...flag (CLI only)OVERBLAST_API_KEYenvironment variablenew 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 meThe 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 accesscategory:*— 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 chunksbusinessData / 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— whenexpiresAtis supplied, this is the signed form…?signed=<token>. The token carries{ id, cid, exp }HMAC-signed withWEBCHAT_LINK_SECRET; the worker rejects scans past expiry without a DB lookup. WithoutexpiresAt,urlequalsunsignedUrl.
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:
fromPhoneE164must be one of the workspace'sshared-numbers.- The platform refuses calls when balance <
CALL_MIN_CHARGE_CENTS. CatchOverblastErrorwithstatus === 402to surface this. goalis the system prompt the agent runs with.taskInstructionsadds 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 idCLI 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:
- Subscribe to
message.received(andcall.endedif calling) viawebhooks.create. Verify the signature, hand the payload to the agent. - Read context with
context.forConversation(...)so the agent has contact memory + open todos + recent messages. - Reply through the right channel:
conversations.sendDmfor social DMswebchat.replyfor website chatemail.reply(oremail.createThreadto start one)calls.createto place a call
- 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 existingThe 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 --noEmitThe 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 testWithout 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.
