@inception-emails/ghl-client
v0.1.0
Published
Shared GoHighLevel client for the Inception Emails 3-app system (marketing, checkout, onboarding).
Readme
@inception-emails/ghl-client
Shared GoHighLevel client for the Inception Emails 3-app system (marketing site, checkout, onboarding).
Wraps the GHL v2 REST API with:
- TypeScript types for the 9 custom fields, 29 tags, and 2 pipelines provisioned in the Inception Emails sub-account (
oPTc9Dv3gSsB3uQmYdBd). - DNC short-circuit on every write — contacts tagged
compliance:dncare never mutated. - Token-bucket rate limiting (default 10 req/s, burst 100) to stay under GHL's ~100 req / 10s per-location ceiling.
- Exponential-backoff retry on
429and5xx;4xx(other) throws immediately. - Stage-name → stage-ID resolution (cached) so callers reference stages by their human name.
- Schema-drift detection on construction in non-production environments.
Install
npm install @inception-emails/ghl-clientNode ≥20.
Use
import { createGhlClient } from '@inception-emails/ghl-client';
const ghl = createGhlClient({
pit: process.env.GHL_LOCATION_API_KEY!, // Private Integration Token, prefix pit-
locationId: 'oPTc9Dv3gSsB3uQmYdBd', // Inception Emails sub-account
});
const { contactId, created } = await ghl.upsertContact({
email: '[email protected]',
firstName: 'Jane',
companyName: 'Acme MSP',
source: 'src:website-form',
});
await ghl.addTags(contactId, ['stage:disco-booked']);
await ghl.setCustomFields(contactId, { vertical: 'MSP', tier: 'Starter' });
await ghl.createOpportunity({
pipeline: 'sales',
stage: 'Disco Booked',
contactId,
name: 'Acme MSP — Starter pilot',
monetaryValue: 5000,
});Public API
All methods throw GhlClientError on failure (loud-not-silent — a dropped contact is a lost lead). DNC contacts make addTags, setCustomFields, and updateContact resolve as no-ops; removeTags is intentionally NOT short-circuited so compliance:dnc itself can be cleared on re-opt-in.
ghl.upsertContact({ email?, phone?, firstName?, lastName?, companyName?, source? })
ghl.findContactByEmail(email)
ghl.findContactByPhone(phone)
ghl.updateContact(contactId, { email?, phone?, firstName?, lastName?, companyName? })
ghl.addTags(contactId, [...tags])
ghl.removeTags(contactId, [...tags])
ghl.setCustomFields(contactId, { vertical?, tier?, monthly_volume_target?, ... })
ghl.createOpportunity({ pipeline, stage, contactId, name, monetaryValue?, status? })
ghl.moveOpportunity(opportunityId, { pipeline, stage, status? })Tag names
src:{website-form,calendly,checkout,onboarding,referral,cold-outbound}
vertical:{msp,functional-med,property-maint,dental,life-insurance,mortgage}
stage:{lead,disco-booked,disco-completed,pitched,active-client,churned,reactivated,lost}
tier:{starter,accelerator,growth,enterprise}
engagement:{hot,warm,cold,dead}
compliance:dncPassing a tag that isn't on this list is a TypeScript compile error.
Custom fields
| Key | GHL dataType | TS type |
|---|---|---|
| vertical | TEXT | string |
| monthly_volume_target | NUMERICAL | number |
| tier | TEXT | string |
| pilot_started_at | DATE | string (ISO YYYY-MM-DD) |
| first_active_campaign_at | DATE | string (ISO YYYY-MM-DD) |
| mrr | NUMERICAL | number |
| lifetime_revenue | NUMERICAL | number |
| disco_call_recording_url | TEXT | string |
| onboarding_completed | CHECKBOX | boolean |
companyName is a GHL standard field (not custom). Pass it via upsertContact/updateContact — never via setCustomFields.
Pipelines + stages
| Pipeline key | Stages (in order) |
|---|---|
| 'sales' | Lead → Disco Booked → Disco Completed → Pitched → Won → Lost |
| 'active-client' | Onboarding → First Campaign → Steady State → At Risk → Renewed → Churned |
The TypeScript signature narrows stage to the valid stages for the given pipeline.
Errors
Every failure throws a GhlClientError with { status, endpoint, method, requestId, body }. The pit is never included.
import { GhlClientError } from '@inception-emails/ghl-client';
try {
await ghl.upsertContact({ email: '[email protected]' });
} catch (err) {
if (err instanceof GhlClientError) {
// err.status, err.endpoint, err.requestId, err.body
}
throw err;
}Configuration
createGhlClient({
pit, // required
locationId, // required
baseUrl: 'https://services.leadconnectorhq.com',
apiVersion: '2021-07-28',
rate: { perSecond: 10, burst: 100 },
maxRetries: 3,
baseBackoffMs: 250,
detectSchemaDrift: process.env.NODE_ENV !== 'production',
});DEBUG=ghl-client env var emits per-request METHOD path attempt=N lines to stderr (no headers, no PIT).
Source of truth
The schema constants in src/schema.ts mirror INTEGRATION-SOURCE-OF-TRUTH.md §5 in the orchestration repo (~/inception-emails-integration). If the live GHL location drifts from those constants, schema-drift detection logs a warning at construction time.
License
MIT
