Skip to main content
State — one ~/.oneshot-gtm/ledger.sqlite is the source of truth for all three surfaces: receipts, prospects, sequence events, cadence state, deal outcomes, interviews, target queue, triggers, bounces, and sender assignments. ONESHOT_GTM_HOME relocates the whole directory. Secrets~/.oneshot-gtm/.env, chmod 600, auto-loaded on first import. Server — single-user, local-first, binds 127.0.0.1 only. Dashboard routes rely on that local boundary. Keep unsigned intake endpoints private or protect them at your reverse proxy; the LinkedIn reply endpoint has its own bearer authentication.

Trigger webhooks

Two JSON endpoints feed warm product signals through the normal ICP filter and review queue:
  • POST /api/triggers/signup requires name, email, and phone; optional fields are signupContext, callWindow, and linkedinUrl. Accepted ICP matches enqueue the concierge play.
  • POST /api/triggers/cal-no-show requires name, email, company, missedAt, and rescheduleLink; optional fields are phone, whatTheyWanted, and linkedinUrl. Accepted ICP matches enqueue demo-no-show.
Valid matches return 202; ICP rejections return 200 with accepted: false; malformed JSON or fields return 400. Signup deliveries deduplicate by lowercase email, while no-shows deduplicate by lowercase email + missedAt. Set WEBHOOK_SECRET to authenticate both endpoints. Send X-Webhook-Signature: t=<unix-seconds>,v1=<hex> where the hex value is an HMAC-SHA256 of <timestamp>.<raw JSON body>. Signed deliveries are accepted for five minutes and may be used only once. When the secret is unset, intake remains unsigned for backwards-compatible local use.

LinkedIn reply webhook

Public LinkedIn inbox APIs require partner approval, so OneShot exposes a provider-neutral intake that Expandi, Zapier, Make, n8n, or another automation can map into. Set a random bearer secret in /setup or ~/.oneshot-gtm/.env:
Then send one stable event ID per actual reply. At least one of linkedinUrl or email is required; when both match different prospects the request is rejected.
Successful responses include duplicate, prospectId, cadencesStopped, and inFlightSends. Retries of the same source + eventId succeed without applying the event twice. The endpoint stops OneShot email; the source tool remains responsible for stopping its own LinkedIn automation.

Stack

Bun 1.3+ · Turborepo with a Bun catalog · Vitest 4 · oxlint + oxfmt · TypeScript 6 (verbatimModuleSyntax, noUncheckedIndexedAccess, noImplicitOverride) · Vite 8 + React 19 + TanStack Router/Query + Base UI + Tailwind 4 · tsdown for the server bundle · bun:sqlite · BYO LLM via OpenRouter, OpenAI or Anthropic. Plain async/await throughout — no monadic abstractions to learn before reading the code. Keeps it forkable.