API Reference
Every REST endpoint — paths, methods, auth tier, and side effects
Internal engineering reference for the CRM V2 API. The app is a Next.js (App Router)
application; every endpoint below is an App-Router route handler under
src/app/api/.
Generated from a full read of every
route.tsfile undersrc/app/api/. Last verified against the source on 2026-05-20.
Conventions
Base URL
All paths are relative to the deployment origin (production:
https://trygpsleaders.com). Every documented path is prefixed with /api.
Authentication
Auth is a 7-day JWT (HS256, JWT_SECRET). Two transports are accepted, checked
in this order by getAuthUser():
Authorization: Bearer <token>headertokenHttpOnly cookie (set byPOST /api/authandPUT /api/auth)
The token payload is { userId, email, role }. On every request the user is
re-loaded from the DB and rejected if isActive === false.
Auth tiers used in this document:
- Public / Webhook — no JWT. Webhooks (
/api/webhooks/*) verify an HMAC signature instead;/api/pay/*and/api/proposals/*rely on an unguessable per-invoicepaymentToken;/api/healthandPUT /api/authare fully open. - Authenticated — any active user with a valid token (
getAuthUser). Role is not checked (ADMINorREP). - Admin-only —
requireAdmin(); rejects non-ADMINwith403.
Helpers (src/lib/auth.ts):
getAuthUser(req)→ user record ornullunauthorized()→401 {"error":"Unauthorized"}requireAdmin(req)→{user}or aResponse(401if no token,403if not admin)
Response & error conventions
- Success bodies are typically
{ data: ... }. List endpoints that paginate also return{ pagination: { page, limit, total, totalPages } }. Some endpoints return ad-hoc shapes ({ success: true },{ accounts: [...] },{ connected: bool },{ crmCalendarEvents, mainCalendarEvents }, etc.) — noted per-endpoint. - Errors are
{ error: "message" }with an HTTP status:400validation,401unauthenticated,403forbidden,404not found,409conflict,413payload too large,500server,501integration not configured,502upstream provider failure. - Pagination query params:
page(default1),limit(default50, capped at100).
Side-effect glossary
Activity rows are an append-only audit/timeline log. External integrations
referenced below: Stripe (payments), QuickBooks (customer/invoice/payment
sync, invoice PDFs), Gmail (OAuth send/sync), Google Calendar/Drive/Places,
Resend (transactional email), Slack (notifications), PlusVibe (outbound
sequences), Hunter / Prospeo / Reoon / Trestle / Brave (contact enrichment),
Cal.com (bookings), android-sms-gateway (SMS).
Auth
POST /api/auth
- Auth: Public
- Purpose: Login with email + password.
- Body:
email,password - Success:
{ token, user }; also setstokenHttpOnly cookie (Max-Age=604800). - Side effects: Updates
user.lastLoginAt. - Returns
401on invalid credentials or inactive user.
PUT /api/auth
- Auth: Public — first-user setup only
- Purpose: Create the first (ADMIN) user. Refuses once any user exists.
- Body:
email,password,firstName,lastName? - Success:
201 { token, user }; setstokencookie. - Side effects: Creates
Userwith roleADMINinside a transaction that rejects (403 SETUP_ALREADY_COMPLETED) ifuser.count() > 0.
GET /api/auth/google/start
- Auth: Authenticated
- Purpose: Begin Google OAuth (Gmail modify, Calendar, userinfo.email,
Drive readonly).
302redirect to Google consent. - Side effects: Sets short-lived
google_oauth_stateCSRF cookie.
GET /api/auth/google/callback
- Auth: Authenticated (redirects to
/settings?error=auth_requiredif absent) - Purpose: Google OAuth callback. Exchanges code, fetches userinfo,
upserts an
EmailAccountwith Gmail tokens.302to/settings. - External: Google OAuth token + userinfo endpoints.
- Side effects: Creates/updates
EmailAccount(refresh + access token, expiry). CSRF-checksgoogle_oauth_statecookie againststateparam.
Health
GET /api/health
- Auth: Public
- Purpose: Liveness probe. Returns
{ status, service, version, timestamp }.
Users
GET /api/users
- Auth:
?active=true→ Authenticated (returns minimal active-user list). Otherwise → Admin-only (full user list with invite metadata). - Success:
{ data: [users] }
POST /api/users
- Auth: Admin-only
- Purpose: Invite a managed user; generates a temp password.
- Body:
email,firstName,lastName,role(ADMIN|REP),phone? - Success:
201 { data: { user, tempPassword } } - Side effects: Creates
Userwith hashed temp password,invitedById/invitedAt.
GET /api/users/me
- Auth: Authenticated
- Purpose: Current user's profile (includes inviter).
PATCH /api/users/me
- Auth: Authenticated
- Purpose: Self-service profile update.
- Body:
firstName?,lastName?,email?,phone?,currentPassword?+newPassword?(password change requires both;newPassword≥ 8 chars). - Returns
409on email collision.
PATCH /api/users/[id]
- Auth: Admin-only
- Purpose: Update another user's
firstName,lastName,phone,role,isActive. Guards: cannot disable self, cannot demote the last active admin.
DELETE /api/users/[id]
- Auth: Admin-only
- Purpose: Soft-delete a user (sets
isActive = false). Cannot delete self.
PATCH /api/users/[id]/email-account
- Auth: Admin-only
- Purpose: Assign (or clear, with
null) anEmailAccountto a user. Ensures one account per user (detaches any others in a transaction). - Body:
emailAccountId(string | null)
GET /api/users/[id]/sms-gateway
- Auth: Admin-only
- Purpose: Fetch the user's SMS gateway config (password not returned).
POST /api/users/[id]/sms-gateway
- Auth: Admin-only
- Purpose: Create an SMS gateway for the user.
- Body:
label,gatewayUrl,username,password,fromNumber,isActive? - Side effects: Stores
passwordencrypted (encryptSecret); normalizesfromNumberto E.164.
PATCH /api/users/[id]/sms-gateway
- Auth: Admin-only
- Purpose: Update the user's existing gateway (partial). Same fields as POST.
DELETE /api/users/[id]/sms-gateway
- Auth: Admin-only
- Purpose: Delete all SMS gateways for the user. Returns
{ data: { deleted } }.
Companies
GET /api/companies
- Auth: Authenticated (results filtered by
companyVisibilityFilter) - Purpose: Paginated company list, enriched with
primaryContact,lifetimeValue(sum of PAID invoices),activeDeals(open-stage lead count),lastActivityAt. - Query:
page,limit,search(name/phone),status,partner=true
POST /api/companies
- Auth: Authenticated
- Purpose: Create a company.
- Body:
name(required),website,phone,industry,icpTag,businessLabel,status(defaultLEAD),source,vendor - Side effects: Fire-and-forget
syncCustomerToQB(QuickBooks).
GET /api/companies/[id]
- Auth: Authenticated (
userCanAccessCompany;404if not visible) - Purpose: Full company detail — addresses, contacts, leads, devices,
invoices, tasks, assigned rep, plus an
openLeadTimelineblock.
PATCH /api/companies/[id]
- Auth: Authenticated
- Purpose: Update company fields (
name,website,phone,industry,icpTag,businessLabel,status,source,vendor,salesNote,taxable,assignedRepId). - Side effects: Logs a
status_changedActivity when status changes.
GET /api/companies/[id]/addresses
- Auth: Authenticated
- Purpose: List a company's addresses (default billing/shipping first).
POST /api/companies/[id]/addresses
- Auth: Authenticated
- Purpose: Add a company address.
- Body:
label,type(BILLING|SHIPPING|BOTH),addressLine1,addressLine2?,city,state,zip,country?(defaultUS),notes?,isDefaultBilling?,isDefaultShipping? - Side effects: Setting a default unsets the prior default in a transaction.
PATCH /api/companies/[id]/addresses/[addressId]
- Auth: Authenticated
- Purpose: Update an address; same fields. Re-evaluates default flags.
DELETE /api/companies/[id]/addresses/[addressId]
- Auth: Authenticated
- Purpose: Delete an address.
GET /api/companies/[id]/cards
- Auth: Authenticated
- Purpose: List saved Stripe cards for the company.
- External: Stripe — lists payment methods; creates a Stripe customer on first access if none exists.
- Success:
{ data: [{ id, brand, last4, expMonth, expYear, isDefault, createdAt }] }
POST /api/companies/[id]/cards
- Auth: Authenticated
- Purpose: Create a Stripe SetupIntent for saving a card.
- External: Stripe SetupIntent.
- Success:
{ clientSecret, publishableKey }
DELETE /api/companies/[id]/cards/[paymentMethodId]
- Auth: Authenticated
- Purpose: Detach a saved card. Verifies the card belongs to the company's Stripe customer. External: Stripe.
POST /api/companies/[id]/enrich
- Auth: Admin-only
- Purpose: Manual company enrichment (website, description, Google rating, reviews, hero photo, maps URL).
- External: Brave business search, Google Places.
- Side effects: Updates company fields; logs a
company_enrichActivity.
GET /api/companies/[id]/notes
- Auth: Authenticated
- Purpose: List company-scoped notes (oldest first), with author.
POST /api/companies/[id]/notes
- Auth: Authenticated
- Purpose: Create a company note. Body:
body
PATCH /api/companies/[id]/notes/[noteId]
- Auth: Authenticated — author only (
403otherwise) - Purpose: Edit own note. Logs a
note_editedActivity.
DELETE /api/companies/[id]/notes/[noteId]
- Auth: Authenticated — author only
- Purpose: Delete own note. Logs a
note_deletedActivity.
Leads
GET /api/leads
- Auth: Authenticated (filtered by
viaCompanyVisibilityFilter) - Purpose: Paginated lead list with contact, company, tags, score, next task.
- Query:
page,limit,search,stage,source,icp
POST /api/leads
- Auth: Authenticated
- Purpose: Manually create a lead.
- Body:
contactId(required),companyId,source,stage(defaultNEW_LEAD),sourceCampaign,estimatedUnits,estimatedRevenue - Side effects: Creates
LeadScore; logs alead_createdActivity.
GET /api/leads/[id]
- Auth: Authenticated (
userCanAccessLead;404if not visible) - Purpose: Full lead detail — contact, company, tags, open tasks, activities (50), active sequence runs, invoices, score.
PATCH /api/leads/[id]
- Auth: Authenticated
- Purpose: Update lead fields (
stage,source,estimatedUnits,estimatedRevenue,lostReason) and/or nestedcontactfields. - Side effects: A
stagechange is routed throughchangeLeadStage()(its own stage-transition Activity/side effects).
DELETE /api/leads/[id]
- Auth: Authenticated
- Purpose: Soft-delete — sets stage to
BOGUS. Logs astage_changedActivity.
PATCH /api/leads/[id]/stage
- Auth: Authenticated
- Purpose: Move a lead to a new stage via
changeLeadStage(). - Body:
stage
GET /api/leads/[id]/tags
- Auth: Authenticated
- Purpose: List a lead's active tags.
POST /api/leads/[id]/tags
- Auth: Authenticated
- Purpose: Apply a tag to a lead. Body:
tagSlug - Side effects (substantial):
- Logs
tag_appliedActivity. icp-*tags update the company'sicpTag/businessLabel.spoke-*,won,dnctags cancel active precall sequence runs and remove precall tags.- PlusVibe:
won/dnccallstopLeadSequences; other spoke tags callpauseLeadSequences(failures logged asplusvibe_*_failedActivity). dnc→ stageLOST, setsisDnc;won→ stageWON.- Disposition tags start a
SequenceRun(schedules first step, precreates tasks; logssequence_started). - Upserts
LeadScore(engagement bump).
- Logs
DELETE /api/leads/[id]/tags?tagSlug=...
- Auth: Authenticated
- Purpose: Remove an active tag.
dncremoval clearsisDnc/lostReason. Logs atag_removedActivity.
GET /api/leads/[id]/notes
- Auth: Authenticated
- Purpose: List company-scoped notes for the lead's company.
400if the lead has nocompanyId.
POST /api/leads/[id]/notes
- Auth: Authenticated
- Purpose: Create a note (stored against the lead's company). Body:
body
PATCH /api/leads/[id]/notes/[noteId]
- Auth: Authenticated — author only
- Purpose: Edit own note. Logs a
note_editedActivity.
DELETE /api/leads/[id]/notes/[noteId]
- Auth: Authenticated — author only
- Purpose: Delete own note. Logs a
note_deletedActivity.
Contacts
GET /api/contacts
- Auth: Authenticated
- Purpose: Paginated contact list. Query:
search,companyId,page,limit
POST /api/contacts
- Auth: Authenticated
- Purpose: Create a contact (
emailorphonerequired). - Body:
firstName,lastName,email,phone,companyId,title,source,isSmsEligible,notes - Returns
409(withexistingContactId/existingCompanyId) on duplicate email. - Side effects: Logs
contact_addedActivity ifcompanyIdset.
GET /api/contacts/[id]
- Auth: Authenticated
- Purpose: Contact detail with company + lead count.
PATCH /api/contacts/[id]
- Auth: Authenticated
- Purpose: Update a contact. Recomputes
fullName/emailNormalized. Returns409on email conflict.
DELETE /api/contacts/[id]
- Auth: Authenticated
- Purpose: Hard-delete a contact. Logs
contact_deletedActivity if linked to a company.
POST /api/contacts/[id]/find-direct-email
- Auth: Admin-only
- Purpose: Discover an owner email via Hunter (email finder or domain search).
- External: Hunter.
501ifHUNTER_API_KEYunset,502on provider error. - Side effects: Upserts a contact for the discovered email; logs
owner_email_discoveredActivity.
POST /api/contacts/[id]/find-direct-mobile
- Auth: Admin-only
- Purpose: Discover an owner mobile number via Prospeo.
- External: Prospeo.
501ifPROSPEO_API_KEYunset. - Side effects: Updates contact phone fields (
phoneE164,phoneType,isSmsEligible,phoneVerifiedAt); logsowner_mobile_discoveredActivity.
POST /api/contacts/[id]/verify-email
- Auth: Admin-only
- Purpose: Verify the contact's email via Reoon.
- External: Reoon.
501ifREOON_API_KEYunset. - Side effects: Sets
emailStatus/emailVerifiedAt; logscontact_email_verifiedActivity.
POST /api/contacts/[id]/verify-phone
- Auth: Admin-only
- Purpose: Verify the contact's phone via Trestle (
lookupPhone). - External: Trestle.
501ifTRESTLE_API_KEYunset. - Side effects: Sets
phoneType/isSmsEligible/phoneVerifiedAt; logscontact_phone_verifiedActivity.
Invoices
GET /api/invoices
- Auth: Authenticated
- Purpose: Paginated invoice list with company, lead, line items, payments, fulfillment orders.
- Query:
status,companyId,page,limit
POST /api/invoices
- Auth: Authenticated
- Purpose: Create a draft invoice.
- Body:
companyId(required),leadId,lineItems[](required;{description, quantity, unitPrice, productCode}),dueDate,notes,agreementTemplateId,taxRateId,backendId,carrierId,shippingTypeId,paymentTermId,contractTermId,shippingCost - Side effects: Generates a unique
INV-YYYY-NNNNnumber (counter inSetting); setspaymentToken; computes tax (only if company is taxable); logsinvoice_createdActivity; fire-and-forgetsyncInvoiceToQB.
GET /api/invoices/[id]
- Auth: Authenticated
- Purpose: Full invoice detail — company (+contacts, addresses), lead, line items, payments, fulfillment orders/items, devices, carrier.
PATCH /api/invoices/[id]
- Auth: Authenticated
- Purpose: Action-discriminated invoice mutations. Body
action:send— DRAFT → SENT. Auto-creates aFulfillmentOrder; resolves a recipient email (optionalrecipientEmailmust be on the lead/company); sends theinvoice-senttransactional email (Resend); logsinvoice_sentActivity.void— → VOID; cancels pending fulfillment orders.record_payment— Bodyamount,method,reference,notes. Creates aPayment; recalculates balance/status (PAID/PARTIAL); fire-and-forgetsyncPaymentToQB; on full payment runspromoteOnInvoicePaid, SlacknotifyInvoicePaid/notifyDealWon, sendspayment-received(customer, with QB PDF attachment if available) andinvoice-paid-alert(to Nick) emails; revalidates the dashboard cache; logspayment_receivedActivity.force_qb_resync— fire-and-forgetsyncInvoiceToQB; logsinvoice_qb_resync_requestedActivity.- No
action— generic update ofnotes/dueDate.
POST /api/invoices/[id]/admin-charge-intent
- Auth: Admin-only
- Purpose: Create (or reuse) a Stripe PaymentIntent to charge a card on file for the invoice balance.
- External: Stripe (PaymentIntent search/list/create, CustomerSession).
- Side effects: Logs
admin_charge_initiatedActivity. - Success:
{ clientSecret, publishableKey, customerSessionClientSecret, hasCustomer } - Returns
400if the invoice is not payable.
GET /api/invoices/[id]/serials
- Auth: Authenticated
- Purpose: List devices (serial numbers) linked to the invoice.
POST /api/invoices/[id]/serials
- Auth: Authenticated
- Purpose: Assign serial numbers to an invoice. Body:
serialNumbers[] - Side effects: Creates/updates
Devicerows (statusASSIGNED), gets/creates aFulfillmentOrder, createsFulfillmentItems; logsdevice_assignedActivity.
Payments
GET /api/payments/[id]/details
- Auth: Admin-only
- Purpose: Payment detail enriched with Stripe card brand/last4 and approval (charge) id when the payment is a Stripe PaymentIntent.
- External: Stripe (PaymentIntent retrieve).
partialData: trueif Stripe retrieve fails.
Pay (public — token-protected)
These endpoints are unauthenticated; the per-invoice paymentToken in the URL is
the only secret.
GET /api/pay/[token]
- Auth: Public (token)
- Purpose: Public invoice/proposal payload — invoice, line items, company, rendered agreement markdown.
- Side effects: Fires
trackInvoiceView(skipped if an authenticated CRM user is viewing): always logs aninvoice_viewedActivity; with a 60s dedupe window, logsinvoice_viewed_notified, sends SlacknotifyInvoiceViewed, and sends theinvoice-viewedemail to Nick (Resend).
POST /api/pay/[token]/checkout
- Auth: Public (token)
- Purpose: Create a Stripe PaymentIntent for the invoice balance. Requires the invoice to be signed and payable.
- External: Stripe (PaymentIntent + CustomerSession).
- Success:
{ clientSecret, publishableKey, customerSessionClientSecret }
GET /api/pay/[token]/receipt
- Auth: Public (token)
- Purpose: Stream the paid-invoice PDF (
application/pdf). Requires statusPAIDand aqbInvoiceId. - External: QuickBooks (
fetchInvoicePdf).502on PDF fetch failure.
POST /api/proposals/[token]/sign
- Auth: Public (token)
- Purpose: E-sign the invoice agreement. Body:
signedName(≥ 3 chars). - Side effects: Renders an agreement snapshot, stores
signedName/signedAt/signedIpAddr/signedUserAgent/signedAgreementSnapshot; logsagreement_signedActivity; SlacknotifyProposalSigned. 400if already signed or invoice is VOID.
Inbox / Conversations
GET /api/inbox
- Auth: Authenticated
- Purpose: Unified inbox — conversations sorted by unread then recency.
- Query:
channel(SMS|EMAIL|CALL),unread=true,archived=true
PATCH /api/inbox
- Auth: Authenticated
- Purpose: Mark a conversation read and/or archive it.
- Body:
conversationId,markRead?,archive?
GET /api/inbox/messages?leadId=...
- Auth: Authenticated
- Purpose: Unified chronological timeline (emails, SMS, notes, events) for a
lead, plus the list of email threads available for reply.
400if the lead has nocompanyId.
POST /api/inbox/sync
- Auth: Authenticated
- Purpose: Pull new Gmail messages for all tracked threads on operator-assigned Gmail accounts.
- External: Gmail API.
- Side effects: Creates
EmailMessagerows; for inbound lead-linked messages upsertsConversation, logsemail_receivedActivity, SlacknotifyInboundEmail, and auto-pauses sequences when the sender exactly matches the lead contact's email.
POST /api/conversations
- Auth: Authenticated
- Purpose: Find-or-create a
Conversationfor a(leadId, channel)pair (used by the "Start Conversation" deep link). - Body:
leadId(required),channel(defaultEMAIL) - Success:
{ data: { id, created } }
POST /api/email/send
- Auth: Authenticated
- Purpose: Send an email via the user's connected Gmail account, to a lead
(
leadId) or a raw address (to). - Body:
leadId?/to?,subject,bodyText/bodyHtml/body,gmailThreadId?,inReplyTo?,attachments?[](driveorinline) - External: Gmail (send); Google Drive (fetch/share large attachments).
Attachments > 25 MB total: inline →
413; Drive files → converted to share links appended to the body. - Side effects: Persists
EmailThread/EmailMessage; for lead-linked sends upsertsConversationand logs anemail_sentActivity.
GET /api/email/drive-access-token
- Auth: Admin-only
- Purpose: Return a valid Gmail/Drive OAuth access token for the primary Gmail account (used by the client-side Drive picker).
GET /api/email-accounts
- Auth: Admin-only
- Purpose: List all
EmailAccounts (id,emailAddress,displayName,userId). Returns{ accounts: [...] }.
SMS
POST /api/sms
- Auth: Authenticated
- Purpose: Send an SMS to a lead's contact.
- Body:
leadId(required),bodyortemplateSlug - Validations: contact must have a phone, be SMS-eligible, and not be DNC.
- External: SMS gateway (
sendSms). Resolves template variables. - Side effects: Creates an
SmsMessage, upsertsConversation, logs ansms_sentActivity. - Success:
{ success, smsId, provider, error? }
Tasks
GET /api/tasks
- Auth: Authenticated
- Purpose: Paginated task list. Query:
completed,priority,type,leadId,page,limit
POST /api/tasks
- Auth: Authenticated
- Purpose: Create a task (assigned to the current user).
- Body:
leadId?,companyId?,title(required),description,taskType,priority,dueAt,durationMin?,pushToCalendar? - External: Google Calendar (pushes an event when
pushToCalendarand adueAtare set andGPS_CALENDAR_IDis configured). - Side effects: Logs
task_createdActivity (ifleadId); revalidates the dashboard cache.
GET /api/tasks/[id]
- Auth: Authenticated
- Purpose: Task detail with lead/contact/company.
PATCH /api/tasks/[id]
- Auth: Authenticated
- Purpose: Update a task (complete, reschedule, edit).
- Body:
title?,description?,priority?,dueAt?,durationMin?,isCompleted? - External: Google Calendar — completing deletes the linked event; editing calendar-relevant fields updates it.
- Side effects: Logs
task_completedActivity on completion; revalidates the dashboard cache.
DELETE /api/tasks/[id]
- Auth: Authenticated
- Purpose: Delete a task (and its Google Calendar event). Revalidates the dashboard cache.
Pipeline & Dashboard
GET /api/pipeline
- Auth: Authenticated
- Purpose: Leads grouped by pipeline stage with per-stage counts and revenue.
- Filter: Returns only leads whose
Company.status = 'LEAD'or whosecompanyIdis null. Leads on companies flipped toCUSTOMER,PARTNER,CONTACT,INACTIVE, orDEADare excluded from both the aggregate and the per-stage card list. RBAC visibility filter still applies on top.
GET /api/dashboard
- Auth: Authenticated
- Purpose: Home-screen rollup — task summary, pipeline snapshot, MTD revenue,
recent positive replies. Backed by a 60s
unstable_cache(dashboardtag).
GET /api/activities
- Auth: Authenticated
- Purpose: Paginated activity feed. Query:
leadId,companyId,page,limit
Taxonomy & Reference Data
GET /api/taxonomies
- Auth: Authenticated
- Purpose: List taxonomy values. With
?type=returns a flat list; without, returns values grouped by type. Query:type,active(true/false/all)
POST /api/taxonomies
- Auth: Admin-only
- Purpose: Create a taxonomy value. Body:
type,value,sortOrder?,active?.409on duplicate.
PATCH /api/taxonomies/[id]
- Auth: Authenticated
- Purpose: Update a taxonomy value (
value,sortOrder,active).Inconsistency: create is Admin-only but edit/delete are Authenticated.
DELETE /api/taxonomies/[id]
- Auth: Authenticated
- Purpose: Delete a taxonomy value.
GET /api/taxonomy/values?type=...
- Auth: Authenticated
- Purpose: Active values of a single taxonomy type (lean
{id, value}list). Valid types:INDUSTRY,SOURCE,VENDOR,CELLULAR_CARRIER,BACKEND,PAYMENT_INTERVAL,SHIPPING_TYPE,PAYMENT_METHOD.
GET /api/form-options
- Auth: Authenticated (ADMIN or REP; explicit
403otherwise) - Purpose: Bundle of active invoice-form reference data — agreement templates, tax rates, backends, carriers, shipping types, payment terms, contract terms.
GET /api/settings
- Auth: Authenticated (non-admins get
qb_*secret keys filtered out) - Purpose: Return all settings as a key→value object.
PATCH /api/settings
- Auth: Admin-only
- Purpose: Upsert a single setting. Body:
key,value
Tags
GET /api/tags
- Auth: Authenticated
- Purpose: List tags. Query:
category,active(true/false/all)
POST /api/tags
- Auth: Admin-only
- Purpose: Create a custom tag (
categoryforced toCUSTOM,isSystemfalse). Body:slug([a-z0-9-]+),name,color?,description?.409on duplicate slug.
PATCH /api/tags/[id]
- Auth: Authenticated
- Purpose: Update a tag (
name,color,sortOrder,isActive,description).Inconsistency: create is Admin-only, edit/delete are Authenticated.
DELETE /api/tags/[id]
- Auth: Authenticated
- Purpose: Delete a tag.
403for system tags;409if the tag is in active use on any lead.
Message Templates
GET /api/message-templates
- Auth: Authenticated
- Purpose: List active templates. Query:
channel(SMS|EMAIL),category(SEQUENCE|CANNED)
POST /api/message-templates
- Auth: Authenticated
- Purpose: Create a
CANNEDtemplate (auto-generates a unique slug). - Body:
name,channel,body,subject?(EMAIL only)
PATCH /api/message-templates/[id]
- Auth: Authenticated
- Purpose: Update a template (
name,subject,body,isActive).403forSEQUENCE(system) templates.
DELETE /api/message-templates/[id]
- Auth: Authenticated
- Purpose: Delete a template.
403forSEQUENCEtemplates.
Products
GET /api/products
- Auth: Authenticated
- Purpose: Product catalog list. Query:
search(name/sku),active(true/false/all)
PATCH /api/products/[id]
- Auth: Authenticated
- Purpose: Toggle a product's
activeflag. Body:active(boolean)
Sequences
GET /api/sequences
- Auth: Authenticated
- Purpose: List sequence definitions with active-run counts.
GET /api/sequences/[tagSlug]
- Auth: Authenticated
- Purpose: Get a single sequence definition.
PATCH /api/sequences/[tagSlug]
- Auth: Admin-only
- Purpose: Update a sequence definition (
name,description,isActive,steps). Steps are validated (numbering, delays, actions, branches) and step templates are checked againstMessageTemplatechannel/existence; steps are renumbered on save.
GET /api/sequences/[tagSlug]/runs
- Auth: Authenticated
- Purpose: Up to 10 active runs for the sequence, with lead/contact/company.
POST /api/sequences/runs/[runId]/cancel
- Auth: Authenticated
- Purpose: Cancel an active sequence run (
cancelSequenceRun). - Side effects: Removes the matching active lead tag; logs a
sequence_run_canceled_manualActivity.400if the run is not active.
QuickBooks
GET /api/quickbooks/connect
- Auth: Admin-only
- Purpose: Build the QuickBooks OAuth authorization URL; stores CSRF state
in
Setting. Returns{ url }.
GET /api/quickbooks/callback
- Auth: Authenticated (redirects to
/settings?error=auth_requiredif absent) - Purpose: QuickBooks OAuth callback. Validates
state, exchanges the code for tokens (exchangeCodeForTokens),302to/settings?qb=connected.
POST /api/quickbooks/disconnect
- Auth: Admin-only
- Purpose: Clear all QuickBooks tokens from
Setting.
GET /api/quickbooks/status
- Auth: Authenticated
- Purpose: Connection status —
{ connected, companyName? }. Pings the QB CompanyInfo endpoint.
POST /api/quickbooks/sync
- Auth: Admin-only
- Purpose: Force-sync one entity to QuickBooks. Body:
type(customer|invoice|payment),id.502on sync failure.
GET /api/quickbooks/sync
- Auth: Authenticated
- Purpose: Sync coverage stats — counts of companies/invoices/payments with vs. without a QB id.
POST /api/quickbooks/sync-items
- Auth: Admin-only
- Purpose: Import the product/service catalog from QuickBooks
(
syncItemsFromQB).502on failure.
Devices, SIMs & Fulfillment
GET /api/devices
- Auth: Authenticated
- Purpose: Paginated device list with company + SIM, plus status counts.
- Query:
status,companyId,search,page,limit
POST /api/devices
- Auth: Admin-only
- Purpose: Create one device or bulk-create (
devices[]). Bulk skips duplicate serial numbers and reports them.
GET /api/sims
- Auth: Authenticated
- Purpose: Paginated SIM-card list with linked device, plus status counts.
- Query:
status,search,page,limit
POST /api/sims
- Auth: Admin-only
- Purpose: Create one SIM or bulk-create (
sims[], dedup on ICCID).
GET /api/fulfillment
- Auth: Authenticated
- Purpose: List fulfillment orders (up to 100) with company, invoice, items.
- Query:
status
PATCH /api/fulfillment
- Auth: Admin-only
- Purpose: Action-discriminated fulfillment mutations. Body:
orderId,action:pick— assigndeviceIds(+ optionalsimAssignments) to the order; updates device/SIM statuses toASSIGNED; order →PICKING.ship— requirestrackingNumber(+carrier); order →SHIPPED, devices →SHIPPED; logsdevice_shippedActivity; SlacknotifyOrderShipped; sends theshippedtransactional email (Resend).deliver— order →DELIVERED.
Imports
POST /api/import
- Auth: Admin-only
- Purpose: Import contacts/leads from a parsed CSV (
contacts[]JSON). Dedups on email and normalized phone; maps stage strings; creates Company/Contact/Lead per row. Returns per-row created/skipped/error counts.
POST /api/imports/leads
- Auth: Admin-only
- Purpose: Bulk lead ingestion from a Google Maps CSV (multipart upload).
- Query:
mode=dry_run(no writes, preview) ormode=commit(default). - Form fields:
file(CSV, required),icpTag(required),businessLabel?,sourceLabel?,assignedRepId? - Side effects (commit mode): Each row flows through
ingestLead()(Company/Contact/Lead create/merge). Returns per-row results (created/merged/skipped/error). Bulk enrichment is off by default.
AI
POST /api/ai
- Auth: Authenticated
- Purpose: AI assistance for a lead. Body:
action(draft|summarize|suggest),leadId,channel? - External: AI provider (
draftReply/summarizeLead/suggestNextAction). - Read-only — no DB writes.
Calendar
GET /api/calendar/events
- Auth: Authenticated
- Purpose: List events from the CRM calendar and the connected Gmail user's
main calendar between
from/toISO timestamps; CRM events are tagged withisCrmTask/taskId. - External: Google Calendar. Query:
from,to(required ISO).
POST /api/google/calendar/test
- Auth: Authenticated
- Purpose: Diagnostic — write a throwaway test event to the CRM calendar.
- External: Google Calendar. Requires
GPS_CALENDAR_ID.
Lead Photos
GET /api/lead-photos/[filename]
- Auth: Authenticated
- Purpose: Stream a cached lead/business photo (
image/jpeg) from the server'sdata/lead-photosdirectory. Filename is whitelisted by regex.
Webhooks (public — signature-verified)
These endpoints take no JWT; each verifies a provider signature instead.
POST /api/webhooks/stripe
- Auth: Public — verifies the
stripe-signatureheader againstSTRIPE_WEBHOOK_SECRET(constructEvent). - Purpose: Handle
checkout.session.completedandpayment_intent.succeededto record invoice payments. - Side effects: Idempotently creates a
Payment(dedup onstripePaymentId); recalculates invoice balance/status; on full payment runspromoteOnInvoicePaid; fire-and-forgetsyncPaymentToQB; SlacknotifyPaymentReceived/notifyDealWon; sendspayment-received(with QB PDF) andinvoice-paid-alertemails (Resend); revalidates the dashboard cache.
POST /api/webhooks/cal
- Auth: Public — verifies
x-cal-signature-256(HMAC-SHA256) againstCAL_WEBHOOK_SECRET. - Purpose: Handle Cal.com
BOOKING_CREATED/BOOKING_RESCHEDULED/BOOKING_CANCELLED. - Side effects: Upserts
Contact/Company/Leadfor new bookings; creates/updatesBookingrows (idempotent oncalBookingUid); logsbooking_created/booking_rescheduled/booking_cancelledActivity.
POST /api/webhooks/plusvibe
- Auth: Public — verifies the
signatureheader (HMAC-SHA256) againstPLUSVIBE_WEBHOOK_SECRET. - Purpose: Ingest a PlusVibe positive-reply / new lead.
- Side effects: Normalizes the payload, detects ICP from campaign name,
delegates to
ingestLead()(Company/Contact/Lead create/merge + enrichment); logslead_createdorpositive_reply_receivedActivity; creates aConversationfor new leads; syncs the PlusVibe email thread; SlacknotifyPositiveReply; revalidates the dashboard cache.
POST /api/webhooks/sms-gateway
- Auth: Public — verifies
x-signature+x-timestamp(HMAC-SHA256 overbody + timestamp) againstSMS_GATEWAY_SECRET. - Purpose: Receive inbound SMS from android-sms-gateway.
- Side effects: Resolves the contact/lead/owning gateway by phone number;
creates an inbound
SmsMessage; upsertsConversation; logssms_receivedActivity; SlacknotifyInboundSms; auto-pauses sequences on an exact phone-number match; bumpsLeadScoreengagement.
Endpoint Count
87 route files, totalling 131 HTTP method-handlers:
| Domain | Endpoints (method-handlers) |
|---|---|
| Auth | 4 |
| Health | 1 |
| Users | 12 |
| Companies | 16 |
| Leads | 14 |
| Contacts | 9 |
| Invoices | 7 |
| Payments | 1 |
| Pay (public) | 4 |
| Inbox / Conversations | 5 |
| 3 | |
| SMS | 1 |
| Tasks | 5 |
| Pipeline & Dashboard | 3 |
| Taxonomy & Reference Data | 8 |
| Tags | 4 |
| Message Templates | 4 |
| Products | 2 |
| Sequences | 5 |
| QuickBooks | 7 |
| Devices, SIMs & Fulfillment | 6 |
| Imports | 2 |
| AI | 1 |
| Calendar | 2 |
| Lead Photos | 1 |
| Webhooks | 4 |
| Total | 131 |
The per-domain table counts each exported HTTP method separately (e.g. the
/api/authroute file exports bothPOSTandPUT, counted as 2). There are 87 distinct route files exporting 131 method-handlers total.