GPS Leaders

Help

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.ts file under src/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():

  1. Authorization: Bearer <token> header
  2. token HttpOnly cookie (set by POST /api/auth and PUT /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-invoice paymentToken; /api/health and PUT /api/auth are fully open.
  • Authenticated — any active user with a valid token (getAuthUser). Role is not checked (ADMIN or REP).
  • Admin-onlyrequireAdmin(); rejects non-ADMIN with 403.

Helpers (src/lib/auth.ts):

  • getAuthUser(req) → user record or null
  • unauthorized()401 {"error":"Unauthorized"}
  • requireAdmin(req){user} or a Response (401 if no token, 403 if 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: 400 validation, 401 unauthenticated, 403 forbidden, 404 not found, 409 conflict, 413 payload too large, 500 server, 501 integration not configured, 502 upstream provider failure.
  • Pagination query params: page (default 1), limit (default 50, capped at 100).

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 sets token HttpOnly cookie (Max-Age=604800).
  • Side effects: Updates user.lastLoginAt.
  • Returns 401 on 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 }; sets token cookie.
  • Side effects: Creates User with role ADMIN inside a transaction that rejects (403 SETUP_ALREADY_COMPLETED) if user.count() > 0.

GET /api/auth/google/start

  • Auth: Authenticated
  • Purpose: Begin Google OAuth (Gmail modify, Calendar, userinfo.email, Drive readonly). 302 redirect to Google consent.
  • Side effects: Sets short-lived google_oauth_state CSRF cookie.

GET /api/auth/google/callback

  • Auth: Authenticated (redirects to /settings?error=auth_required if absent)
  • Purpose: Google OAuth callback. Exchanges code, fetches userinfo, upserts an EmailAccount with Gmail tokens. 302 to /settings.
  • External: Google OAuth token + userinfo endpoints.
  • Side effects: Creates/updates EmailAccount (refresh + access token, expiry). CSRF-checks google_oauth_state cookie against state param.

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 User with 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 409 on 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) an EmailAccount to 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 password encrypted (encryptSecret); normalizes fromNumber to 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 (default LEAD), source, vendor
  • Side effects: Fire-and-forget syncCustomerToQB (QuickBooks).

GET /api/companies/[id]

  • Auth: Authenticated (userCanAccessCompany; 404 if not visible)
  • Purpose: Full company detail — addresses, contacts, leads, devices, invoices, tasks, assigned rep, plus an openLeadTimeline block.

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_changed Activity 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? (default US), 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_enrich Activity.

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 (403 otherwise)
  • Purpose: Edit own note. Logs a note_edited Activity.

DELETE /api/companies/[id]/notes/[noteId]

  • Auth: Authenticated — author only
  • Purpose: Delete own note. Logs a note_deleted Activity.

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 (default NEW_LEAD), sourceCampaign, estimatedUnits, estimatedRevenue
  • Side effects: Creates LeadScore; logs a lead_created Activity.

GET /api/leads/[id]

  • Auth: Authenticated (userCanAccessLead; 404 if 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 nested contact fields.
  • Side effects: A stage change is routed through changeLeadStage() (its own stage-transition Activity/side effects).

DELETE /api/leads/[id]

  • Auth: Authenticated
  • Purpose: Soft-delete — sets stage to BOGUS. Logs a stage_changed Activity.

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_applied Activity.
    • icp-* tags update the company's icpTag/businessLabel.
    • spoke-*, won, dnc tags cancel active precall sequence runs and remove precall tags.
    • PlusVibe: won/dnc call stopLeadSequences; other spoke tags call pauseLeadSequences (failures logged as plusvibe_*_failed Activity).
    • dnc → stage LOST, sets isDnc; won → stage WON.
    • Disposition tags start a SequenceRun (schedules first step, precreates tasks; logs sequence_started).
    • Upserts LeadScore (engagement bump).

DELETE /api/leads/[id]/tags?tagSlug=...

  • Auth: Authenticated
  • Purpose: Remove an active tag. dnc removal clears isDnc/lostReason. Logs a tag_removed Activity.

GET /api/leads/[id]/notes

  • Auth: Authenticated
  • Purpose: List company-scoped notes for the lead's company. 400 if the lead has no companyId.

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_edited Activity.

DELETE /api/leads/[id]/notes/[noteId]

  • Auth: Authenticated — author only
  • Purpose: Delete own note. Logs a note_deleted Activity.

Contacts

GET /api/contacts

  • Auth: Authenticated
  • Purpose: Paginated contact list. Query: search, companyId, page, limit

POST /api/contacts

  • Auth: Authenticated
  • Purpose: Create a contact (email or phone required).
  • Body: firstName, lastName, email, phone, companyId, title, source, isSmsEligible, notes
  • Returns 409 (with existingContactId/existingCompanyId) on duplicate email.
  • Side effects: Logs contact_added Activity if companyId set.

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. Returns 409 on email conflict.

DELETE /api/contacts/[id]

  • Auth: Authenticated
  • Purpose: Hard-delete a contact. Logs contact_deleted Activity 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. 501 if HUNTER_API_KEY unset, 502 on provider error.
  • Side effects: Upserts a contact for the discovered email; logs owner_email_discovered Activity.

POST /api/contacts/[id]/find-direct-mobile

  • Auth: Admin-only
  • Purpose: Discover an owner mobile number via Prospeo.
  • External: Prospeo. 501 if PROSPEO_API_KEY unset.
  • Side effects: Updates contact phone fields (phoneE164, phoneType, isSmsEligible, phoneVerifiedAt); logs owner_mobile_discovered Activity.

POST /api/contacts/[id]/verify-email

  • Auth: Admin-only
  • Purpose: Verify the contact's email via Reoon.
  • External: Reoon. 501 if REOON_API_KEY unset.
  • Side effects: Sets emailStatus/emailVerifiedAt; logs contact_email_verified Activity.

POST /api/contacts/[id]/verify-phone

  • Auth: Admin-only
  • Purpose: Verify the contact's phone via Trestle (lookupPhone).
  • External: Trestle. 501 if TRESTLE_API_KEY unset.
  • Side effects: Sets phoneType/isSmsEligible/phoneVerifiedAt; logs contact_phone_verified Activity.

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-NNNN number (counter in Setting); sets paymentToken; computes tax (only if company is taxable); logs invoice_created Activity; fire-and-forget syncInvoiceToQB.

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 a FulfillmentOrder; resolves a recipient email (optional recipientEmail must be on the lead/company); sends the invoice-sent transactional email (Resend); logs invoice_sent Activity.
    • void — → VOID; cancels pending fulfillment orders.
    • record_payment — Body amount, method, reference, notes. Creates a Payment; recalculates balance/status (PAID/PARTIAL); fire-and-forget syncPaymentToQB; on full payment runs promoteOnInvoicePaid, Slack notifyInvoicePaid/notifyDealWon, sends payment-received (customer, with QB PDF attachment if available) and invoice-paid-alert (to Nick) emails; revalidates the dashboard cache; logs payment_received Activity.
    • force_qb_resync — fire-and-forget syncInvoiceToQB; logs invoice_qb_resync_requested Activity.
    • No action — generic update of notes/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_initiated Activity.
  • Success: { clientSecret, publishableKey, customerSessionClientSecret, hasCustomer }
  • Returns 400 if 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 Device rows (status ASSIGNED), gets/creates a FulfillmentOrder, creates FulfillmentItems; logs device_assigned Activity.

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: true if 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 an invoice_viewed Activity; with a 60s dedupe window, logs invoice_viewed_notified, sends Slack notifyInvoiceViewed, and sends the invoice-viewed email 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 status PAID and a qbInvoiceId.
  • External: QuickBooks (fetchInvoicePdf). 502 on 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; logs agreement_signed Activity; Slack notifyProposalSigned.
  • 400 if 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. 400 if the lead has no companyId.

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 EmailMessage rows; for inbound lead-linked messages upserts Conversation, logs email_received Activity, Slack notifyInboundEmail, and auto-pauses sequences when the sender exactly matches the lead contact's email.

POST /api/conversations

  • Auth: Authenticated
  • Purpose: Find-or-create a Conversation for a (leadId, channel) pair (used by the "Start Conversation" deep link).
  • Body: leadId (required), channel (default EMAIL)
  • Success: { data: { id, created } }

Email

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?[] (drive or inline)
  • 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 upserts Conversation and logs an email_sent Activity.

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), body or templateSlug
  • 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, upserts Conversation, logs an sms_sent Activity.
  • 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 pushToCalendar and a dueAt are set and GPS_CALENDAR_ID is configured).
  • Side effects: Logs task_created Activity (if leadId); 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_completed Activity 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 whose companyId is null. Leads on companies flipped to CUSTOMER, PARTNER, CONTACT, INACTIVE, or DEAD are 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 (dashboard tag).

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?. 409 on 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 403 otherwise)
  • 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 (category forced to CUSTOM, isSystem false). Body: slug ([a-z0-9-]+), name, color?, description?. 409 on 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. 403 for system tags; 409 if 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 CANNED template (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). 403 for SEQUENCE (system) templates.

DELETE /api/message-templates/[id]

  • Auth: Authenticated
  • Purpose: Delete a template. 403 for SEQUENCE templates.

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 active flag. 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 against MessageTemplate channel/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_manual Activity. 400 if 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_required if absent)
  • Purpose: QuickBooks OAuth callback. Validates state, exchanges the code for tokens (exchangeCodeForTokens), 302 to /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. 502 on 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). 502 on 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 — assign deviceIds (+ optional simAssignments) to the order; updates device/SIM statuses to ASSIGNED; order → PICKING.
    • ship — requires trackingNumber (+ carrier); order → SHIPPED, devices → SHIPPED; logs device_shipped Activity; Slack notifyOrderShipped; sends the shipped transactional 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) or mode=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/to ISO timestamps; CRM events are tagged with isCrmTask/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's data/lead-photos directory. 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-signature header against STRIPE_WEBHOOK_SECRET (constructEvent).
  • Purpose: Handle checkout.session.completed and payment_intent.succeeded to record invoice payments.
  • Side effects: Idempotently creates a Payment (dedup on stripePaymentId); recalculates invoice balance/status; on full payment runs promoteOnInvoicePaid; fire-and-forget syncPaymentToQB; Slack notifyPaymentReceived/notifyDealWon; sends payment-received (with QB PDF) and invoice-paid-alert emails (Resend); revalidates the dashboard cache.

POST /api/webhooks/cal

  • Auth: Public — verifies x-cal-signature-256 (HMAC-SHA256) against CAL_WEBHOOK_SECRET.
  • Purpose: Handle Cal.com BOOKING_CREATED / BOOKING_RESCHEDULED / BOOKING_CANCELLED.
  • Side effects: Upserts Contact/Company/Lead for new bookings; creates/updates Booking rows (idempotent on calBookingUid); logs booking_created/booking_rescheduled/booking_cancelled Activity.

POST /api/webhooks/plusvibe

  • Auth: Public — verifies the signature header (HMAC-SHA256) against PLUSVIBE_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); logs lead_created or positive_reply_received Activity; creates a Conversation for new leads; syncs the PlusVibe email thread; Slack notifyPositiveReply; revalidates the dashboard cache.

POST /api/webhooks/sms-gateway

  • Auth: Public — verifies x-signature + x-timestamp (HMAC-SHA256 over body + timestamp) against SMS_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; upserts Conversation; logs sms_received Activity; Slack notifyInboundSms; auto-pauses sequences on an exact phone-number match; bumps LeadScore engagement.

Endpoint Count

87 route files, totalling 131 HTTP method-handlers:

DomainEndpoints (method-handlers)
Auth4
Health1
Users12
Companies16
Leads14
Contacts9
Invoices7
Payments1
Pay (public)4
Inbox / Conversations5
Email3
SMS1
Tasks5
Pipeline & Dashboard3
Taxonomy & Reference Data8
Tags4
Message Templates4
Products2
Sequences5
QuickBooks7
Devices, SIMs & Fulfillment6
Imports2
AI1
Calendar2
Lead Photos1
Webhooks4
Total131

The per-domain table counts each exported HTTP method separately (e.g. the /api/auth route file exports both POST and PUT, counted as 2). There are 87 distinct route files exporting 131 method-handlers total.