Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 94 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -608,24 +608,33 @@ bento.V1.Tags.createTag({

### Sequences

Retrieve sequences and their associated email templates.
Sequences power drip campaigns, onboarding flows, and other time-based journeys by chaining multiple email templates with configurable delays. The SDK mirrors the public Sequences API for fetching sequences, creating new sequence emails, and updating template content. Refer to the [Sequences API docs](https://docs.bentonow.com/sequences_api#get-sequences) for full request/response details.

#### getSequences

Retrieves all sequences for the site, including their email templates.
Calls `GET /v1/fetch/sequences` and returns every sequence plus the embedded email templates and stats. Pass `{ page }` to paginate through large installs—the SDK appends `site_uuid` automatically.

```javascript
const sequences = await bento.V1.Sequences.getSequences();
// Returns:
// [
import { Analytics } from '@bentonow/bento-node-sdk';

const analytics = new Analytics({
authentication: {
publishableKey: process.env.BENTO_PUBLISHABLE_KEY,
secretKey: process.env.BENTO_SECRET_KEY,
},
siteUuid: process.env.BENTO_SITE_UUID,
});

const sequences = await analytics.V1.Sequences.getSequences({ page: 1 });
// sequences => [
// {
// id: '123',
// id: 'seq-1',
// type: 'sequence',
// attributes: {
// name: 'Welcome Sequence',
// created_at: '2024-01-01T00:00:00Z',
// email_templates: [
// { id: 1, subject: 'Welcome!', stats: null },
// { id: 1, subject: 'Welcome!', stats: { opened: 100, clicked: 50 } },
// { id: 2, subject: 'Getting Started', stats: null }
// ]
// }
Expand All @@ -635,39 +644,61 @@ const sequences = await bento.V1.Sequences.getSequences();

#### createSequenceEmail

Creates a new email template inside a sequence.
Wraps [`POST /v1/fetch/sequences/:id/emails/templates`](https://docs.bentonow.com/sequences_api#create-sequence-email) so you can add messages to a sequence via code. Pass the sequence prefix ID (e.g., `sequence_abc123`) plus the subject/HTML and any optional delay/snippet/editor fields.

```javascript
const createdTemplate = await bento.V1.Sequences.createSequenceEmail('sequence_abc123', {
const createdTemplate = await analytics.V1.Sequences.createSequenceEmail('sequence_abc123', {
subject: 'Welcome to Bento',
html: '<p>Hello {{ visitor.first_name }}</p>',
delay_interval: 'days',
delay_interval_count: 7,
inbox_snippet: 'Welcome to the sequence',
editor_choice: 'plain',
});
```

#### updateSequenceEmail

Sequence emails reuse the Email Templates resource, so updates happen through `analytics.V1.EmailTemplates.updateEmailTemplate` (the same helper documented in the [Email Templates](#email-templates) section). Only `subject` and `html` are patchable today, matching [`PATCH /v1/fetch/emails/templates/:id`](https://docs.bentonow.com/sequences_api#update-sequence-email).

```javascript
await analytics.V1.EmailTemplates.updateEmailTemplate({
id: 12345,
subject: 'Updated subject',
html: '<h1>Updated HTML</h1>',
});
```

### Workflows

Retrieve workflows and their associated email templates.
Workflows (a.k.a. Flows) are Bento’s automation engine for welcome journeys, abandoned-cart nudges, re-engagement loops, and other event-driven campaigns. The SDK surfaces the public Workflows API so you can inspect every flow (including the embedded email templates and their stats) straight from Node. See the [Workflows API reference](https://docs.bentonow.com/workflows_api#get-workflows) for the canonical response schema.

#### getWorkflows

Retrieves all workflows for the site, including their email templates.
Calls `GET /v1/fetch/workflows` and returns an array of workflows. Pass an optional `page` parameter to paginate through large accounts—the SDK automatically injects your `site_uuid` so you only need to provide the page number.

```javascript
const workflows = await bento.V1.Workflows.getWorkflows();
// Returns:
// [
import { Analytics } from '@bentonow/bento-node-sdk';

const analytics = new Analytics({
authentication: {
publishableKey: process.env.BENTO_PUBLISHABLE_KEY,
secretKey: process.env.BENTO_SECRET_KEY,
},
siteUuid: process.env.BENTO_SITE_UUID,
});

const workflows = await analytics.V1.Workflows.getWorkflows({ page: 2 });
// workflows => [
// {
// id: '456',
// id: 'wf-1',
// type: 'workflow',
// attributes: {
// name: 'Onboarding Workflow',
// name: 'Abandoned Cart Recovery',
// created_at: '2024-01-01T00:00:00Z',
// email_templates: [
// { id: 3, subject: 'Step 1', stats: null },
// { id: 4, subject: 'Step 2', stats: null }
// { id: 3, subject: 'Reminder #1', stats: { opened: 42, clicked: 10 } },
// { id: 4, subject: 'Reminder #2', stats: null }
// ]
// }
// }
Expand All @@ -676,38 +707,63 @@ const workflows = await bento.V1.Workflows.getWorkflows();

### Email Templates

Retrieve and update email templates used in sequences and workflows.
Retrieve and update email templates used in sequences and workflows. Both helpers call the public Email Templates API (`GET /v1/fetch/emails/templates/:id` and `PATCH /v1/fetch/emails/templates/:id`), and the SDK automatically injects your `site_uuid` and authentication headers. See the [Email Templates API docs](https://docs.bentonow.com/email_templates_api#get-email-template) for the canonical contract.

#### getEmailTemplate

Retrieves a single email template by ID.
Retrieves a single email template by ID and returns `null` when the Bento API responds with an empty payload. Use this to surface subject lines, HTML, and performance stats inside your own tooling.

```javascript
const template = await bento.V1.EmailTemplates.getEmailTemplate({ id: 123 });
// Returns:
// {
// id: '123',
// type: 'email_template',
// attributes: {
// name: 'Welcome Email',
// subject: 'Welcome to our service!',
// html: '<p>Hello {{ name }}, welcome!</p>',
// created_at: '2024-01-01T00:00:00Z',
// stats: null
// }
// }
import { Analytics } from '@bentonow/bento-node-sdk';

const analytics = new Analytics({
authentication: {
publishableKey: process.env.BENTO_PUBLISHABLE_KEY,
secretKey: process.env.BENTO_SECRET_KEY,
},
siteUuid: process.env.BENTO_SITE_UUID,
});

const template = await analytics.V1.EmailTemplates.getEmailTemplate({ id: 123 });
if (!template) {
console.log('Template not found');
} else {
console.log(template.attributes.subject, template.attributes.stats);
}
```

#### updateEmailTemplate

Updates an email template's subject and/or HTML content.
Updates an email template's subject and/or HTML content via [`PATCH /v1/fetch/emails/templates/:id`](https://docs.bentonow.com/email_templates_api#update-email-template). Only pass the fields you want to change; omitted fields stay untouched. The helper returns the updated template (`null` if Bento responds empty) and bubbles up standard SDK errors such as `NotAuthorizedError`, `RateLimitedError`, or `RequestTimeoutError`.

```javascript
const updatedTemplate = await bento.V1.EmailTemplates.updateEmailTemplate({
id: 123,
subject: 'Updated Subject Line',
html: '<p>Updated HTML content with {{ name }}</p>',
import { Analytics, NotAuthorizedError } from '@bentonow/bento-node-sdk';

const analytics = new Analytics({
authentication: {
publishableKey: process.env.BENTO_PUBLISHABLE_KEY,
secretKey: process.env.BENTO_SECRET_KEY,
},
siteUuid: process.env.BENTO_SITE_UUID,
});

try {
const updatedTemplate = await analytics.V1.EmailTemplates.updateEmailTemplate({
id: 123,
subject: 'Updated Subject Line',
html: '<p>Updated HTML content with {{ name }}</p>',
});

if (updatedTemplate) {
console.log(updatedTemplate.attributes.subject);
}
} catch (error) {
if (error instanceof NotAuthorizedError) {
console.error('Check your Bento credentials or site permissions.');
} else {
throw error;
}
}
```

For detailed information on each module, refer to the [SDK Documentation](https://docs.bentonow.com/subscribers).
Expand Down
18 changes: 17 additions & 1 deletion __tests__/client/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { expect, test, describe, beforeEach, afterEach, mock } from 'bun:test';
import { Analytics } from '../../src';
import { mockOptions } from '../helpers/mockClient';
import { setupMockFetch, lastFetchSignal, resetMockFetchTracking } from '../helpers/mockFetch';
import {
setupMockFetch,
lastFetchSignal,
lastFetchUrl,
resetMockFetchTracking,
} from '../helpers/mockFetch';
import { NotAuthorizedError, RateLimitedError, RequestTimeoutError } from '../../src';

describe('BentoClient', () => {
Expand Down Expand Up @@ -215,6 +220,17 @@ describe('BentoClient', () => {
expect(url.searchParams.get('site_uuid')).toBe(mockOptions.siteUuid);
});

test('omits undefined query parameters when forwarding SDK options', async () => {
setupMockFetch({ data: [] });

await analytics.V1.Sequences.getSequences({ page: undefined });

expect(lastFetchUrl).not.toBeNull();
const url = new URL(lastFetchUrl!);
expect(url.searchParams.has('page')).toBe(false);
expect(url.searchParams.get('site_uuid')).toBe(mockOptions.siteUuid);
});

test('initializes with custom timeout', () => {
const customOptions = {
...mockOptions,
Expand Down
11 changes: 11 additions & 0 deletions __tests__/sequences/sequences.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ describe('BentoSequences', () => {
expect(lastFetchMethod).toBe('GET');
});

test('passes pagination parameters in query string', async () => {
setupMockFetch({ data: [] });

await analytics.V1.Sequences.getSequences({ page: 5 });

expect(lastFetchUrl).toContain('/fetch/sequences');
const url = new URL(lastFetchUrl!);
expect(url.searchParams.get('page')).toBe('5');
expect(url.searchParams.get('site_uuid')).toBe(mockOptions.siteUuid);
});

test('returns empty array when response is empty object', async () => {
setupMockFetch({});

Expand Down
11 changes: 11 additions & 0 deletions __tests__/workflows/workflows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ describe('BentoWorkflows', () => {
expect(lastFetchMethod).toBe('GET');
});

test('passes pagination parameters in query string', async () => {
setupMockFetch({ data: [] });

await analytics.V1.Workflows.getWorkflows({ page: 3 });

expect(lastFetchUrl).toContain('/fetch/workflows');
const url = new URL(lastFetchUrl!);
expect(url.searchParams.get('page')).toBe('3');
expect(url.searchParams.get('site_uuid')).toBe(mockOptions.siteUuid);
});

test('returns empty array when response is empty object', async () => {
setupMockFetch({});

Expand Down
1 change: 1 addition & 0 deletions src/sdk/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ export class BentoClient {

const queryParameters = new URLSearchParams();
for (const [key, value] of Object.entries(body)) {
if (value === undefined || value === null) continue;
queryParameters.append(key, String(value));
}

Expand Down
7 changes: 4 additions & 3 deletions src/sdk/sequences/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { BentoClient } from '../client';
import type { DataResponse } from '../client/types';
import type { EmailTemplate } from '../email-templates/types';
import type { CreateSequenceEmailParameters, Sequence } from './types';
import type { CreateSequenceEmailParameters, GetSequencesParameters, Sequence } from './types';

export class BentoSequences {
private readonly _url = '/fetch/sequences';
Expand All @@ -11,10 +11,11 @@ export class BentoSequences {
/**
* Returns all of the sequences for the site, including their email templates.
*
* @param parameters Optional pagination parameters (e.g., { page: 2 })
* @returns Promise\<Sequence[]\>
*/
public async getSequences(): Promise<Sequence[]> {
const result = await this._client.get<DataResponse<Sequence[]>>(this._url);
public async getSequences(parameters: GetSequencesParameters = {}): Promise<Sequence[]> {
const result = await this._client.get<DataResponse<Sequence[]>>(this._url, parameters);

if (!result || Object.keys(result).length === 0) return [];
return result.data ?? [];
Expand Down
4 changes: 4 additions & 0 deletions src/sdk/sequences/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export type Sequence = BaseEntity<SequenceAttributes>;

export type SequenceDelayInterval = 'minutes' | 'hours' | 'days' | 'months';

export type GetSequencesParameters = {
page?: number;
};

export type CreateSequenceEmailParameters = {
subject: string;
html: string;
Expand Down
7 changes: 4 additions & 3 deletions src/sdk/workflows/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { BentoClient } from '../client';
import type { DataResponse } from '../client/types';
import type { Workflow } from './types';
import type { GetWorkflowsParameters, Workflow } from './types';

export class BentoWorkflows {
private readonly _url = '/fetch/workflows';
Expand All @@ -10,10 +10,11 @@ export class BentoWorkflows {
/**
* Returns all of the workflows for the site, including their email templates.
*
* @param parameters Optional pagination parameters (e.g., { page: 2 })
* @returns Promise\<Workflow[]\>
*/
public async getWorkflows(): Promise<Workflow[]> {
const result = await this._client.get<DataResponse<Workflow[]>>(this._url);
public async getWorkflows(parameters: GetWorkflowsParameters = {}): Promise<Workflow[]> {
const result = await this._client.get<DataResponse<Workflow[]>>(this._url, parameters);

if (!result || Object.keys(result).length === 0) return [];
return result.data ?? [];
Expand Down
4 changes: 4 additions & 0 deletions src/sdk/workflows/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ export type WorkflowAttributes = {
};

export type Workflow = BaseEntity<WorkflowAttributes>;

export type GetWorkflowsParameters = {
page?: number;
};