@growstack/sdk

中文文档
0.4.0发布于 2026/11/01MITTypeScriptES Module零依赖

Typed, retry-safe client for the Growstack public API

原始使用文档
文档目录13 节
  1. Send a transactional message
  2. Upsert a contact
  3. Sync a product account
  4. Properties and field history
  5. Request a double opt-in subscription
  6. Track an event
  7. Upload a brand logo
  8. Open a customer billing page
  9. Handle errors
  10. Calls, retries and cancellation
  11. Iterate record pages
  12. Develop and publish
  13. Pipelines and deals

@growstack/sdk

Typed client for the whole Growstack public API. MIT licensed, ESM with TypeScript declarations, using global fetch on Node 20+, Bun, Vercel and edge runtimes.

npm install @growstack/sdk
import { createGrowstack, GrowstackError } from '@growstack/sdk'

const growstack = createGrowstack({ apiKey: process.env.GROWSTACK_API_KEY! })
// For self-hosting: createGrowstack({ apiKey, baseUrl: 'https://marketing.example.com' })

Every instance keeps its own key, base URL and optional fetch(request) implementation. Use a secret key on your server. A public key belongs only in the browser operations allowed by your workspace's tracking policy.

In a signed-in browser, omit apiKey and use the app's origin. The SDK sends no Authorization header and keeps cookies through Fetch's credentials option, which defaults to same-origin. Set credentials: 'include' when your deployment allows cookie-authenticated requests from another origin.

const personal = createGrowstack({ baseUrl: window.location.origin })
const accounts = await personal.connections.list()

Connections and member connection summaries require a session. API keys cannot access them.

Send a transactional message

Requires messages:send, an authorised sender on the key, and a verified sending domain. The message key is the workspace's published Simple message template. Other published template keys work the same way.

const sent = await growstack.messages.create({
  body: {
    to: '[email protected]',
    from: { address: '[email protected]' },
    purpose: 'transactional',
    template: {
      key: 'message',
      variables: { subject: 'Your receipt', body: 'Thank you for your order.' },
    },
  },
}, { idempotencyKey: 'order:example-42:receipt' })
console.log(sent.id)

Upsert a contact

Requires people:write. An email or external identifier finds an existing contact or creates one. Omitted fields stay unchanged; conflicting identities are never merged silently.

const { person, created } = await growstack.people.upsert({
  body: {
    identifier: { email: '[email protected]' },
    name: 'Ana',
    country: 'ES',
    source: 'shop',
  },
})

Sync a product account

Requires people:write. The brand slug and immutable account id identify the account. Keep its source revision increasing, using one counter or timestamp kind. Equal or older revisions return ignored: true and leave all data intact.

const { account, ignored } = await growstack.productAccounts.upsert({
  path: { brand: 'heralded', externalId: 'workspace-42' },
  body: {
    revision: 5,
    name: 'Example workspace',
    status: 'paying',
    plan: 'Growth',
    currency: 'EUR',
    mrr: '79.00',
    users: [{ identifier: { email: '[email protected]' }, role: 'owner' }],
  },
})

Omitted fields stay unchanged. Supplied users replace membership. Users accept contact-upsert identifiers; an external identifier may include an email. Conflicting users remain unlinked and needsReview is true. Review explicitly with productAccountUsers.review, choosing link or keep_apart. The latter keeps the product identity without attaching the conflicting email.

Properties and field history

properties manages definitions for contact, company, deal, lead and product_account. personProperties exposes the same contact definitions. Shared properties have one key and type across their appliesTo objects.

const definitions = await growstack.properties.list({ path: { object: 'contact' } })
const plan = definitions.data.find(property => property.key === 'plan')
const growth = plan?.optionDefinitions?.find(option => option.label === 'Growth')
const contact = await growstack.people.get({ path: { id: person.id } })
console.log(contact.attributes.plan) // Current label, such as 'Growth'
console.log(contact.attributeOptions?.plan) // { id: '<option UUID>', label: 'Growth' }

Select writes accept a label or an option id in attributes. Multiselects accept an ordered array of labels or ids. Reads keep labels in attributes and expose {id, label} values in attributeOptions, using an array for a multiselect in its stored order. Unmatched values are kept as written and omitted from attributeOptions; invalidProperties names their fields. Renaming an option preserves its id and resolves saved references to its old label. optionDefinitions updates options by id and can archive or merge them.

peopleChanges.list and companiesChanges.list return field changes with their old and new values, actor and time. Rewriting the same value creates no change. Tenant exports include definitions and history.

Request a double opt-in subscription

Requires consent:write. Create a topic with double opt-in, or use an existing topic configured that way.

await growstack.topics.create({
  body: { key: 'product-updates', name: 'Product updates', doubleOptIn: true },
})
await growstack.subscriptions.create({
  body: { address: '[email protected]', topic: 'product-updates', source: 'shop' },
})

This creates a pending subscription. The recipient confirms through the emailed hosted link; the SDK never confirms consent itself. Consent evidence fields details and occurredAt require a secret key. Public keys and signed-in sessions cannot submit them.

Track an event

Requires events:write on a secret key. Browser tracking uses a public key and its workspace's event allowlist.

await growstack.track({
  body: { event: 'order.completed', email: '[email protected]', data: { order: 'example-42' } },
}, { idempotencyKey: 'order:example-42:completed' })

Requires brands:manage. Pass a PNG, JPEG or WebP Blob or File, up to 200 KiB, with its MIME type. The client sends the original bytes and content type.

await growstack.brandsLogo.update({
  path: { id: brand.id },
  body: new Blob([imageBytes], { type: 'image/webp' }),
})

Open a customer billing page

Requires a secret key with billing:write. The company must be a billing customer and the brand must be assigned to an issuing company. The customer needs no invoices yet; the page then shows an empty list. Test mode is refused. Use an HTTPS return URL. The returned link works once within five minutes; create it when the customer chooses to open billing.

const portal = await growstack.billingPortalSessions.create({
  body: {
    companyId: customer.companyId,
    brandId: brand.id,
    returnUrl: 'https://app.example.com/account',
    locale: 'en',
  },
})
// Redirect your customer to portal.url.

The page supports English and Romanian, shows invoices and quote PDFs, and takes invoice payments. It cannot accept quotes. Access ends after twelve idle hours or when its issuing key is removed or loses billing:write.

Handle errors

try {
  await growstack.people.upsert({
    body: { identifier: { email: '[email protected]' }, source: 'shop' },
  })
} catch (error) {
  if (error instanceof GrowstackError) {
    console.error(error.status, error.type, error.code, error.message, error.requestId)
    // error.issues contains field paths and messages when supplied by the API.
  } else {
    throw error // Network failures and cancellation retain their original errors.
  }
}

An error response outside the API envelope, such as a gateway's HTML 502, throws GrowstackError with its HTTP status, type: 'internal', code: 'unexpected_response' and requestId: null.

Calls, retries and cancellation

Methods accept the generated { path, query, body } input followed by optional { idempotencyKey, signal }, and return the decoded success body. Operations use the resource and action from Growstack's explicit operation map, such as growstack.campaigns.send and growstack.peopleEmailAddresses.list. growstack.raw exposes every operation id, such as raw.peopleUpsert and raw.trackCreate, with the same inputs and transport.

Every POST receives an automatic UUID idempotency key unless you supply one. Generated keys protect retries within one call. Pass your own stable key across separate calls that represent the same action, as in the receipt example. The API's replay window is 24 hours.

GETs and keyed POSTs retry at most three times after the first attempt for network failures, 429, 5xx and 409 idempotency_key_in_use. Each retry keeps the exact key and body. Delays use full jitter over min(8000, 500 × 2^retryIndex) milliseconds, or a longer valid Retry-After in seconds or as an HTTP date. Other 4xx, other 409s and caller cancellation never retry. PUT, PATCH and DELETE never retry.

const controller = new AbortController()
const result = growstack.people.list({ query: { limit: 100 } }, { signal: controller.signal })
// controller.abort() cancels the request or a retry delay.
await result

Iterate record pages

Every operation returning a record page has .all(input, options), including POST queries and nested lists. It yields one record at a time, preserves filters and parent parameters, and stops when hasMore is false.

for await (const person of growstack.people.list.all({ query: { limit: 100 } })) {
  console.log(person.id)
}
for await (const row of growstack.people.query.all({
  body: { filter: { person: 'country', op: 'eq', value: 'ES' }, limit: 100 },
})) {
  console.log(row.person.id)
}

Each POST page gets its own key, reused for retries of that page. If an iterator receives a caller key, it applies to the first page; later pages generate new keys because their request bodies differ. The outbox iterator growstack.events.list.all sends checkpoints as after and stops when caught up, even though the final checkpoint remains available. Use events.list directly to save checkpoints for a later poll.

Develop and publish

The server owns the MIT public document. Export without workers, connections or credentials, then generate with the pinned @hey-api/openapi-ts fetch client:

bun run --cwd apps/server openapi:export
bun run --cwd packages/sdk generate
bun run --cwd packages/sdk build
bun run verify

Commit openapi.json, src/generated and src/bindings.gen.ts. Root verification re-exports and regenerates them, fails on drift, then builds and typechecks the SDK. TypeScript 6.0.3 is a local development dependency because the generator uses its compiler API; the repository's TypeScript 7 still runs root typechecking.

Component authoring uses growstack.components.preview and growstack.components.generate. Repository publishing creates a version through growstack.library.create or growstack.libraryVersions.create, then calls growstack.libraryVersions.publish. The result’s approvalId identifies a required workspace review; publishedAt stays null until approval. The repository’s bun run components CLI builds script dependencies with Bun and uses these same operations. See packages/components/README.md.

bun run version:sdk patch|minor|major|<x.y.z> updates the version and changelog. The sdk-v<version> tag triggers the publishing workflow with npm trusted publishing over OIDC. The overseer configures the trusted publisher and pushes the initial sdk-v0.1.0 tag. Provenance is omitted while the source repository is private and must be enabled when it becomes public. The package contains the built SDK, MIT source and documentation, without generation scripts, server code or tests. The source supports the repository's @growstack/source export condition.

Pipelines and deals

growstack.deals creates, reads, queries, edits, reopens and deletes deals. growstack.pipelines and growstack.pipelineStages manage pipelines and stages; these writes require an owner or admin session. Stage edits preserve the won and lost categories. A deal update can supply required fields together with stageId; expectedStageId refuses a stale move. Query responses include open and weighted totals in the workspace reporting currency and identify unavailable exchange rates. growstack.dealSettings reads and updates the reporting currency and lost reasons.

版本与发布记录

@growstack/sdk 共发布 5 个版本,首次发布于 2026/10/20;过去 12 个月发布了 5 个版本。

版本发布日期距今
0.4.0latest2026/11/012 天前
0.3.02026/10/313 天前
0.2.02026/10/286 天前
0.1.02026/10/2014 天前
0.0.0-stage2026/10/2014 天前

关于 @growstack/sdk 的常见问题

@growstack/sdk 是什么?

用于Growstack公共API的类型化、重试安全的客户端

如何安装 @growstack/sdk?

在项目目录运行 npm install @growstack/sdk,或使用 pnpm add / yarn add / bun add。

@growstack/sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@growstack/sdk 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@growstack/sdk 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@growstack/sdk 可以商用吗?

@growstack/sdk 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@growstack/sdk 还在维护吗?

最新版本 0.4.0 发布于 2026/11/01(2 天前);至今共发布 5 个版本,其中过去 12 个月发布了 5 个;首次发布于 2026/10/20。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。