Use the TypeScript SDK
Install @gtmengine/sdk to upsert accounts and contacts, manage custom fields, and log idempotent activities from your own TypeScript or JavaScript code.
@gtmengine/sdk is a typed client for the GTM Engine public API. It wraps the same endpoints described in Sync records with the public API, with request and response types generated from the API's OpenAPI spec, automatic retries, and typed errors.
Use it to mirror records from your product database, a data warehouse, or an internal tool into GTM Engine. For example, you could create an account for every customer that signs up, a contact for every user, and an activity whenever someone files a support ticket.
Install
npm install @gtmengine/sdkThe SDK works in Node.js 18+, Bun, Deno, and edge runtimes, and ships both ESM and CommonJS builds.
Create a client
Generate an API key under Automation → API. Write calls need a key with read_write access.
import { createGtmClient } from '@gtmengine/sdk'
const gtm = createGtmClient({ apiKey: process.env.GTM_ENGINE_API_KEY! })An API key can read and write every record in your organization. Only use the SDK in server-side code, and never ship the key to a browser or mobile app.
| Option | Default | Description |
|---|---|---|
apiKey | required | Sent as the X-Api-Key header. |
baseUrl | https://api.gtmengine.ai | API host. |
maxRetries | 3 | Retries for 429, 5xx, and network errors, with exponential backoff (and Retry-After when the response includes it). |
fetch | globalThis.fetch | Custom fetch implementation, for example in tests. |
Define custom fields
fields.upsert creates a custom field or updates it in place. For enum and multi-select fields, new options are merged in and existing options are never removed, so it's safe to run on every deploy or process start.
await gtm.fields.upsert({
record_type: 'accounts',
key: 'plan',
display_name: 'Plan',
data_type: 'enum',
enum_values: ['Free', 'Pro', 'Enterprise'],
})
await gtm.fields.upsert({
record_type: 'contacts',
key: 'contact_type',
display_name: 'Contact Type',
data_type: 'enum',
enum_values: ['User', 'Offboarded'],
})List existing fields with gtm.fields.list({ record_type: 'contacts', custom_only: true }).
Upsert accounts and contacts
Accounts match on domain and contacts match on email. If a match exists it's updated; otherwise a new record is created. The response includes created so you can tell which happened.
const { data: account, created } = await gtm.accounts.upsert({
account_name: 'Acme',
domain: 'acme.com',
relationship_type: 'customer',
custom_fields: { plan: 'Pro' },
})
const { data: contact } = await gtm.contacts.upsert({
email: 'jane@acme.com',
first_name: 'Jane',
last_name: 'Doe',
account_ids: [account.id],
custom_fields: { contact_type: 'User' },
})account_ids adds links and never removes existing ones. Custom field values are checked against each field's definition. See custom field values for the rules.
To update a record without renaming it, look it up and patch it:
const [existing] = await gtm.accounts.list({ domain: 'acme.com', limit: 1 })
if (existing) await gtm.accounts.update(existing.id, { custom_fields: { plan: 'Enterprise' } })Log activities
activities.create adds an activity to the timeline of one or more contacts or opportunities. Pass an external_id from your system so retries and re-runs never create duplicates. If an activity with that external_id already exists, it's returned with created: false.
await gtm.activities.create({
external_id: `ticket:${ticket.id}`,
interaction_type: 'support_ticket',
direction: 'inbound',
content: `${ticket.subject}\n\n${ticket.body}`,
tags: ['support-ticket', `priority:${ticket.priority}`],
date_time: ticket.createdAt.toISOString(),
contact_ids: [contact.id],
})interaction_type defaults to custom. Other useful values include support_ticket, form_submission, product_tour, note, and follow_up. Use tags to label your own event types. New activities fire Activity created workflow triggers, so you can automate follow-ups from product events.
Handle errors
Failed requests throw a GtmApiError with the HTTP status, the API's error message, and the raw response body. Transient failures are retried before the error is thrown.
import { GtmApiError } from '@gtmengine/sdk'
try {
await gtm.contacts.upsert({ email: 'jane@acme.com', first_name: 'Jane', last_name: 'Doe' })
} catch (error) {
if (error instanceof GtmApiError && error.status === 400) {
// Validation error, e.g. an unknown custom field key
}
throw error
}Call any endpoint
Every public endpoint has a helper: accounts, contacts, and opportunities have list, get, create, and update. accounts and contacts also have upsert. pipelines, stages, and users have list and get. When you create an opportunity without a stage_id, it starts in the first stage of your default pipeline.
const opportunities = await gtm.opportunities.list({ account_id: account.id })
const stages = await gtm.stages.list({ pipeline_id: pipelineId })gtm.raw is a fully typed openapi-fetch client for any endpoint, if you need the raw response.
const { data, error } = await gtm.raw.GET('/v1/records/opportunities', {
params: { query: { limit: 50 } },
})Request and response types such as Account, Contact, Opportunity, UpsertAccountInput, CreateActivityInput, paths, and components are exported, so you can use them in your own code.
Tips for syncing from your product
- Send events, then read current state. Rather than pushing each change as it happens, queue a lightweight event (for example "user 123 changed") and have a background job read the user's current state and upsert it. Retries and out-of-order events then still converge on the right data.
- Derive
external_idfrom your own IDs, such asticket:<id>orsession:<userId>:<startedAt>, so the same real-world event always maps to one activity. - Backfill with the same code path. Because every call is idempotent, a one-off script that loops over existing customers can call the same sync function as your live events.
Sync records with the public API
Upsert accounts and contacts, write custom field values, manage field definitions, and log idempotent activities through the GTM Engine public API.
Salesforce managed package admin guide
Install the GTM Engine managed package, authorize Salesforce, verify access, and troubleshoot admin setup.