npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@graph8/sdk

v0.308.1

Published

graph8 SDK - CRM, enrichment, sequences, campaigns, and tracking for B2B

Downloads

3,871

Readme

graph8

The most comprehensive GTM SDK. One npm install. Every graph8 capability.

AI coding agents (Claude Code, Codex, Cursor): read docs/agents.md (node_modules/@graph8/sdk/docs/agents.md) first. It covers setup, the two client layers, safety tiers, errors and tested recipes, and links a per-area reference for every operation in docs/api/. It ships with the package, so it matches this version. Run npx @graph8/sdk init-agent to point Claude Code, Codex and Cursor in your project at it automatically.

Install

npm install @graph8/sdk

Quick Start

Client-side (write key)

import { g8 } from '@graph8/sdk';

g8.init({ writeKey: 'YOUR_WRITE_KEY' });

// Track events
g8.track('signup', { plan: 'pro' });

// Identify users
g8.identify('[email protected]', { name: 'John', company: 'Acme' });

// Identify visitor's company from IP
const visitor = await g8.visitors.identify();
// { company_name: 'Acme Corp', industry: 'SaaS', employee_count: '200-500' }

// Listen for high-intent visitors
g8.visitors.onIntent('high', (visitor) => {
  showCTABanner(`${visitor.company_name} is checking us out!`);
});

// Open AI copilot
g8.copilot.open({ greeting: 'How can I help?' });

// Open webchat
g8.chat.open();

// Show booking widget
g8.calendar.show({ username: 'thomas', eventType: 'demo-30min' });

// Progressive form enrichment
const known = await g8.forms.lookup('[email protected]');
// { found: true, known_fields: { name: 'John' }, missing_fields: ['phone'] }

Server-side (API key)

import { g8 } from '@graph8/sdk';

g8.init({ apiKey: 'YOUR_API_KEY' });

// Contacts: create-or-update in one idempotent call (matches work_email, then linkedin_url).
// Use this when the contact may already exist; it is safe to retry.
const { action } = await g8.contacts.upsert({ list_id: 42, work_email: '[email protected]', first_name: 'Jane' });
// action is "created" or "updated"

// Contacts CRUD. create() is create-only: a duplicate throws a 409 G8Error
// whose detail.hint / detail.upsert point back at upsert().
const { data: contacts } = await g8.contacts.list({ company_name: 'Acme', limit: 10 });
const contact = await g8.contacts.create({ work_email: '[email protected]', first_name: 'Jane' });
await g8.contacts.update(contact.id, { job_title: 'VP Sales' });

// Companies
const { data: companies } = await g8.companies.list({ industry: 'SaaS' });
const { data: teamContacts } = await g8.companies.contacts(companies[0].id);

// Lists
const list = await g8.lists.create('Q2 Outbound', 'contacts');
await g8.lists.addContacts(list.id, [contact.id]);
const { data: listContacts } = await g8.lists.contacts(list.id);

// Enrich a person
const person = await g8.enrich.person({ email: '[email protected]' });

// Search 700M+ contacts
const leads = await g8.enrich.search([
  { field: 'seniority_level', operator: 'any_of', value: ['VP', 'Director'] },
  { field: 'company_industry', operator: 'contains', value: ['SaaS'] },
]);

// List sequences and add contacts
const sequences = await g8.sequences.list();
await g8.sequences.add({ sequenceId: sequences[0].id, contactIds: [123], listId: 637 });

// Campaign management
const campaign = await g8.campaigns.create({ name: 'Q2 Push', category: 'Outbound' });
await g8.campaigns.launch(campaign.id);

// Intent signals
const signals = await g8.signals.company('acme.com');
// { score: 87, intent: 'high', signals: ['pricing_page_3x', 'case_study'] }

// Webhook events (push model — verify each delivery server-side)
app.post('/webhooks/graph8', (req, res) => {
  const event = g8.webhooks.constructEvent(
    req.rawBody,                          // raw body string (verify before JSON.parse)
    req.header('X-Studio-Signature'),
    req.header('X-Studio-Timestamp'),
    process.env.G8_WEBHOOK_SECRET,
    { toleranceSeconds: 300 },
  );
  if (event.event === 'engagement.email_replied') slack.send(`${event.data.contact} replied!`);
  res.sendStatus(200);
});

React / Next.js

import { G8Provider, useG8 } from '@graph8/sdk/react';

// Layout
export const Layout = ({ children }: { children: React.ReactNode }) => (
  <G8Provider writeKey="YOUR_WRITE_KEY">{children}</G8Provider>
);

// Any component. `useG8()` exposes the tracking calls, the progressive-form
// helpers, and the raw client — reach the rest of the surface through `g8`.
export const CTA = () => {
  const { track, identify, page, reset, forms, g8 } = useG8();
  return (
    <button
      onClick={() => {
        track('cta_click', { placement: 'hero' });
        g8.copilot.open({ greeting: 'How can I help?' });
      }}
    >
      Talk to us
    </button>
  );
};

Every endpoint: g8.api (generated from the contract)

The clients above are the ergonomic layer — hand-written, opinionated, and only as complete as someone made them. g8.api is the complete one: it is generated from the published OpenAPI contract, so it covers every Developer API operation (the count and a per-area reference are in docs/api/README.md), including surfaces with no hand-written client (Appointments, Radar, App Objects, Ad Campaigns, Sandbox, Landing Pages, Agent, OpenSearch, ClickHouse and more).

import { g8 } from '@graph8/sdk';

g8.init({ apiKey: process.env.G8_API_KEY });

// Grouped by resource, with full autocomplete on inputs and responses.
const bookings = await g8.api.appointments.listBookings({ query: { take: 50 } }); // `take`, not `limit` — the types say so
const competitors = await g8.api.radar.listCompetitors();

// Or address an operation by its stable, published operationId.
const contacts = await g8.api.call('list_contacts_contacts_get', { query: { limit: 50 } });

// Path, query, headers and body are separate and individually typed.
const contact = await g8.api.contacts.getContact({ path: { contact_id: 123 } });

Auto-pagination follows pagination.next_cursor for you:

import { g8 } from '@graph8/sdk';

for await (const contact of g8.api.paginate('list_contacts_contacts_get', { query: { limit: 100 } })) {
  console.log(contact);
}

Every operation carries the side-effect tier and the API-key scope the API publishes for it, so an integration can warn a human before a call that spends credits or deletes something:

import { g8 } from '@graph8/sdk';

const op = g8.api.operation('list_contacts_contacts_get');
// { method: 'GET', path: '/api/v1/contacts', tier: 'read', scope: 'contacts:read', ... }

const risky = g8.api.operations().filter((o) => o.tier === 'billable' || o.tier === 'destructive');
console.log(`${risky.length} operations have side effects worth confirming`);

Contract schemas are available as types under the G8Contract namespace:

import type { G8Contract } from '@graph8/sdk';

function fullName(contact: G8Contract.ContactResponse): string {
  return [contact.first_name, contact.last_name].filter(Boolean).join(' ');
}

Or let the operation map name them for you, with no import of the schema at all:

import { g8, type ApiOutput } from '@graph8/sdk';

type ContactPage = ApiOutput<'list_contacts_contacts_get'>;

async function firstPage(): Promise<ContactPage> {
  return g8.api.call('list_contacts_contacts_get', { query: { limit: 25 } });
}

Two things to know before reaching for it:

  • It returns the raw response envelope. The hand-written clients unwrap .data and rename fields for you; g8.api gives you exactly what the contract declares. Prefer the hand-written client where one exists.
  • It is generated — never hand-edited. sdk/js/src/generated/** is produced by npm run generate from developer-contract/openapi.json, and CI fails if the two disagree. That is what keeps coverage at 100% as the API grows.

Full API Reference

Core (write key)

| Method | Description | |--------|-------------| | g8.init(config) | Initialize SDK | | g8.track(event, props?) | Track event | | g8.identify(userId, props?) | Identify user | | g8.page(props?) | Track page view | | g8.reset() | Clear identity |

Contacts (API key)

| Method | Description | |--------|-------------| | g8.contacts.upsert(contact) | Create or update a contact (PUT /contacts/assert). Matches work_email, then linkedin_url; needs list_id. Idempotent, the recommended way to write a contact | | g8.contacts.list(params?) | List contacts with filters | | g8.contacts.get(id) | Get contact by ID | | g8.contacts.create(contact) | Create a contact. Create-only: a duplicate throws a 409 whose detail.upsert names upsert() | | g8.contacts.update(id, fields) | Update contact (partial) | | g8.contacts.delete(id) | Delete contact |

Companies (API key)

| Method | Description | |--------|-------------| | g8.companies.list(params?) | List companies with filters | | g8.companies.get(id) | Get company by ID | | g8.companies.contacts(id) | Get company's contacts | | g8.companies.update(id, fields) | Update company (partial) | | g8.companies.delete(id) | Delete company |

Lists (API key)

| Method | Description | |--------|-------------| | g8.lists.list(page?, limit?) | List all lists | | g8.lists.create(title, type?) | Create a list | | g8.lists.delete(id) | Delete a list | | g8.lists.contacts(id, page?, limit?) | Get contacts in list | | g8.lists.addContacts(id, contactIds) | Add contacts to list | | g8.lists.removeContacts(id, contactIds) | Remove contacts from list |

Visitor Intelligence (write key)

| Method | Description | |--------|-------------| | g8.visitors.identify() | IP to company resolution | | g8.visitors.score() | Engagement score | | g8.visitors.onIntent(level, cb) | Real-time intent listener |

Forms (write key)

| Method | Description | |--------|-------------| | g8.forms.lookup(email) | Progressive form enrichment |

Copilot (write key)

| Method | Description | |--------|-------------| | g8.copilot.open(config?) | Open AI assistant widget | | g8.copilot.ask(message) | Send message programmatically | | g8.copilot.registerAction(name, fn) | Register custom action | | g8.copilot.close() | Close widget |

Webchat (write key)

| Method | Description | |--------|-------------| | g8.chat.open(config?) | Open chat widget | | g8.chat.send(message) | Send message | | g8.chat.on(event, cb) | Listen for events | | g8.chat.close() | Close widget |

Calendar (write key)

| Method | Description | |--------|-------------| | g8.calendar.show(config) | Show booking modal | | g8.calendar.embed(selector, config) | Inline embed | | g8.calendar.slots(user, slug, range) | Get available slots | | g8.calendar.book(request) | Book programmatically |

Enrichment (API key)

| Method | Description | |--------|-------------| | g8.enrich.person(params) | Look up a person (2 credits, charged only when a match is found). REST: POST /enrichment/lookup/person (POST /enrichment/person is an alias) | | g8.enrich.company(params) | Look up a company (2 credits, charged only when a match is found) | | g8.enrich.verifyEmail(email) | Verify email (1 credit) | | g8.enrich.search(filters) | Search 700M+ contacts (no credits) |

Sequences (API key)

| Method | Description | |--------|-------------| | g8.sequences.list() | List sequences | | g8.sequences.add(config) | Add contacts to sequence |

Campaigns (API key)

| Method | Description | |--------|-------------| | g8.campaigns.list() | List campaigns | | g8.campaigns.create(config) | Create campaign | | g8.campaigns.launch(id) | Launch campaign |

Signals (write key or API key)

| Method | Description | |--------|-------------| | g8.signals.company(domain) | Get intent signals | | g8.signals.stream(domains, cb) | Stream signals (polls 30s) |

With an apiKey, signals use Bearer auth and the key must carry the intent:read scope. Personal keys and keys minted before scoped keys do not, and get a 403, which company() throws as a G8Error explaining the fix (and which stops stream()). With only a writeKey, a failure still resolves to an all-zeros result.

Webhooks (API key)

| Method | Description | |--------|-------------| | g8.webhooks.constructEvent(body, sig, ts, secret, opts?) | Verify a delivery's HMAC signature and return the parsed event (throws WebhookSignatureError) | | g8.webhooks.knownEvents | The known event-type catalog |

The unreleased CRM increment adds crm.record.created, crm.record.updated, crm.record.archived, and crm.record.restored. Their data contains object_slug, record_id, and revision, rather than private field values. The envelope id is a stable event ID; X-Studio-Delivery-Id identifies a subscription's delivery. Verify the original request bytes before processing, and deduplicate by the stable identifiers because delivery is at least once. CRM subscription creation, update, deletion and secret rotation through the API require explicit webhooks:write and objects:read scopes. Removing CRM events also requires these grants. Reading CRM subscriptions or their delivery history requires explicit webhooks:read and objects:read scopes; a subscription list containing CRM subscriptions enforces the same read grants before returning data. These checks refresh credentials and apply even during compatibility scope soak. The backend checks current credential, membership and record access again before sending. Native custom records currently follow workspace-level access; app-owned/delegated authority and configurable Enterprise object grants remain incomplete.

Native custom objects

Use an organization API key with explicit objects:read for schema/record/history reads, objects:write for record create/PATCH, and objects:delete for record archive. Schema create/update/archive require objects:manage and current Admin access through a personal API key. objects.update(slug, { is_archived: false }) restores an object. Object create accepts description and a typed icon. Update also accepts display_attribute_slug for an active, single-value label field. Omit presentation properties to preserve them or pass null to clear them. The current display field must be replaced or cleared before archiving it or making it multivalue. Attribute administration in the unreleased increment uses objects.createAttribute, objects.updateAttribute, and objects.archiveAttribute with the same scope and Admin requirement. Restore with objects.updateAttribute(objectSlug, attributeSlug, { is_archived: false }). Use objects.listAttributes(objectSlug, { include_archived: true }) to find archived fields; normal discovery and record validation use active fields only. Attribute PATCH preserves omitted fields; default_value: null explicitly clears a default. Requires the matching backend.

Unscoped keys receive 403. objects:* grants all object scopes; grant only the operations the integration needs.

import { g8 } from '@graph8/sdk';

g8.init({ apiKey: process.env.G8_API_KEY! });
const { data: objects } = await g8.objects.list();
const { data: attributes } = await g8.objects.listAttributes('invoices');
// Assumes this workspace defines an invoices object with a reference attribute.
const invoice = await g8.objects.createRecord('invoices', { reference: 'INV-1042' });
const history = await g8.objects.history('invoices', invoice.id);

PATCH preserves omitted attributes. Enabled defaults apply only on create; unknown fields return 422 and unique-value collisions return 409. Standard contacts, companies, and deals continue using their existing SDK resources.

Create or update by a unique key

Use an active, single-value unique attribute to match a native custom record. Supply its nonempty value with the fields to write. Existing matches retain their graph8 ID and omitted values; new records receive configured defaults.

import { g8 } from '@graph8/sdk';

g8.init({ apiKey: process.env.G8_API_KEY! });
// Assumes invoices.reference is a unique text attribute in this workspace.
const invoice = await g8.objects.upsertRecord('invoices', 'reference', {
  reference: 'INV-1042',
});
// A conditional update fails with 409 if the record changed or no longer matches.
await g8.objects.upsertRecord('invoices', 'reference', {
  reference: 'INV-1042',
}, { expectedRevision: invoice.revision });

The REST operation is POST /api/v1/objects/{object_slug}/records/upsert with { "matching_attribute": "reference", "values": { "reference": "INV-1042" } }. It requires objects:write, returns 201 for creation or 200 for an update, and accepts optional expected_revision. MCP exposes g8_object_record_upsert with the same fields. Archived records do not match. Invalid or nonunique matching attributes return 422; another unique-field collision returns 409. Legacy values requiring a uniqueness backfill also return 409 rather than creating a duplicate. Repeated upserts can create additional update history; unique-key matching is not a promise of exactly-once side effects.

App Platform

Hosted apps do not carry a permanent org API key. Instead they exchange a longer-lived credential for a short-lived graph8 app token, and the client mints, refreshes, and attaches it for you. Both clients expose a token-bound request() that reaches the existing Developer API resources with the app token.

Browser (PropelAuth token exchange)

import { createGraph8AppClient } from '@graph8/sdk';

// Exchange the signed-in user's PropelAuth token for a short-lived graph8 app
// token. The client refreshes it on/near expiry and attaches
// `Authorization: Bearer <app-token>` to every call — no API key in the browser.
const appClient = createGraph8AppClient({
  appId: 'dapp_your_app',
  tenantOrgId: 'org_customer',
  scopes: ['objects:read'],
  getPropelAuthToken: () => localStorage.getItem('propelauth_token') ?? '',
});

// Reach the existing resources with the app token via request():
const { data: objects } = await appClient.request<{ data: unknown[] }>(
  '/api/v1/app/objects',
  { query: { limit: 10 } },
);

Backend (service credentials)

import { createGraph8ServiceClient } from '@graph8/sdk';

// Exchange service client-credentials for a service token. Every call is pinned
// to the one consented tenant via the `X-Target-Org-Id` header the client adds.
const service = createGraph8ServiceClient({
  clientId: process.env.G8_APP_CLIENT_ID,
  clientSecret: process.env.G8_APP_CLIENT_SECRET,
  tenantOrgId: 'org_customer',
  scopes: ['objects:read', 'objects:write'],
});

const { data: objects } = await service.request<{ data: unknown[] }>('/api/v1/app/objects');

App-token clients use request() for their consented app-owned objects. The g8.objects resource above uses an API key for native workspace objects; it does not substitute for app-token authorization.

Auth Modes

| Mode | Key | Use case | |------|-----|----------| | Client-side | writeKey | Browser apps - tracking, visitor ID, widgets | | Server-side | apiKey | Node.js - enrichment, sequences, campaigns, CRM | | Both | writeKey + apiKey | Full access |

Get your API key at app.graph8.com/settings under MCP & API > API.

X-Target-Org-Id (agency keys only)

Setting the X-Target-Org-Id header on a call switches an agency key (minted with is_agency: true) to one of its authorized client orgs (see g8.agency.clients()). For any other key the server ignores the header without an error: the call runs against the key's own org. /api-keys and /agency/* reject the header with a 400. Once g8.agency.me() or g8.agency.clients() has shown that a key is not an agency key, the SDK logs one console warning the first time that key sends the header. The SDK never makes an extra request just to check.

License

MIT

Conditional custom-record updates

Custom-record responses expose revision. Pass it when saving an interactive edit:

import { g8 } from "@graph8/sdk";

g8.init({ apiKey: "YOUR_API_KEY" });
const projectId = "your-project-record-id";
const record = await g8.objects.getRecord("projects", projectId);
const updated = await g8.objects.updateRecord(
  "projects", record.id, { name: "Updated project" },
  { expectedRevision: record.revision },
);

A stale revision returns HTTP 409 with code revision_conflict; reload the record and reconcile the edit before retrying. Native and app APIs accept expected_revision in the PATCH body, and the MCP update tool accepts the same argument. Omitting it retains unconditional PATCH behavior. Older servers may omit revision; deploy the revision-enabled backend before relying on this precondition. This applies to custom-object records.

Restore a custom record with g8.objects.restoreRecord(objectSlug, recordId). The native API uses POST /api/v1/objects/{objectSlug}/records/{recordId}/restore; MCP exposes g8_object_record_restore. Restoration preserves the original ID and old history, validates the current schema and references, and returns 409 if a unique value has been reused. Validation failures leave the record archived. Ambiguous legacy history returns 422 with archive_snapshot_unavailable rather than guessing its prior values. Repeating a successful restore adds no revision or history entry.

Append or remove multivalue items

import { g8 } from "@graph8/sdk";

g8.init({ apiKey: "YOUR_API_KEY" });
const projectId = "your-project-record-id";
const record = await g8.objects.getRecord("projects", projectId);
await g8.objects.updateRecord("projects", projectId, {}, {
  appendValues: { tags: ["priority"] },
  removeValues: { reviewers: ["former-reviewer-id"] },
  expectedRevision: record.revision,
});

Native and app PATCH APIs use append_values and remove_values; the MCP g8_object_record_update tool accepts those same names. Each field must occur in exactly one of values, append_values, or remove_values. These operations require a multivalue attribute; values still replaces the entire supplied field. Append preserves existing order and adds only canonically distinct items. Remove deletes all matching items. The resulting values must satisfy required, unique, and reference constraints. Changes are atomic and read the current values under a record lock, so concurrent appends do not discard each other.

Repeating an append can leave the values unchanged while still advancing the record revision and history. This is not an idempotency guarantee for events. Deploy the matching backend before using these optional request fields.

Record mutation history

import { g8 } from "@graph8/sdk";

g8.init({ apiKey: "YOUR_API_KEY" });
const firstPage = await g8.objects.changes("projects", "your-project-record-id", { limit: 50 });
if (firstPage.next_cursor) {
  const olderPage = await g8.objects.changes("projects", "your-project-record-id", {
    limit: 50,
    cursor: firstPage.next_cursor,
  });
  console.log(olderPage.entries);
}

The matching native and app APIs expose /objects/{object_slug}/records/{record_id}/changes and /app/objects/{object_slug}/records/{record_id}/changes. The MCP tool is g8_object_record_changes. These additions require the matching backend deployment. Entries include the record revision, action, actor identity/type, source, time, and changed values. Presence flags distinguish a cleared field from a present null. An empty page means no mutation entries were recorded, not that the record never changed; the existing value-history endpoint remains available for older facts. Use the returned opaque cursor for older pages. A storage failure is an error, not an empty history. This interface does not establish broader object/record ACL parity.

Conditional deal updates (unreleased)

Read the canonical deal record through GET /api/v1/objects/deals/records/{id} to obtain its CRM revision, then pass expected_revision to g8.deals.update(id, { name: "Updated", expected_revision: revision }). A stale edit returns HTTP 409; reload and reconcile before retrying. A successful conditional update returns revision. If tenant revision tracking is unavailable, the conditional write returns 503 before mutation. Omitting expected_revision retains the existing PATCH contract. The MCP g8_update_deal tool accepts the same field. Deal UI integration and deployed QA verification remain pending.