Official Node.js and TypeScript client for the Email Funnel AI integration API. Connect with your project key and secret, then call typed, resource-oriented methods — no manual URLs, headers, or JSON.
- Node 18+ (uses the built-in
fetch) — also works in Deno, Bun, and modern browsers - Zero runtime dependencies
- First-class TypeScript types, dual ESM + CommonJS builds
- Fluent, discoverable API with full endpoint coverage
npm install @emailfunnelai/sdk-nodeimport { EmailFunnelAi } from '@emailfunnelai/sdk-node';
const client = new EmailFunnelAi({
projectKey: 'pk_your_project_key',
projectSecret: 'sk_your_secret_key',
});
// baseUrl defaults to https://app.emailfunnel.ai — pass it only for staging or local dev
// 1. Confirm your credentials
await client.validate();
// 2. Get or create a list
const list = await client.lists().create('Newsletter signups');
// 3. Sync a contact into it
await client
.contacts()
.sync({ email: 'jane@example.com', first_name: 'Jane', source_type: 'custom_saas' })
.toList(list.id as string);CommonJS works too:
const { EmailFunnelAi } = require('@emailfunnelai/sdk-node');Every group is reached from the client and reads as resource → verb.
// Single contact
await client.contacts().sync({ email: 'jane@example.com', source_type: 'app' }).toList(listId);
// Bulk (up to 500; the server queues automatically above 100 rows)
await client
.contacts()
.bulk(
[
{ email: 'a@example.com', source_type: 'import' },
{ email: 'b@example.com', source_type: 'import' },
],
'import',
)
.toList(listId);
// Real-time webhook event
await client.contacts().webhook('user.created', { email: 'jane@example.com' }).toList(listId);await client.lists().all();
await client.lists().create('My leads', 'Optional description');
await client.lists().find(listId);await client.bindings().all();
await client.bindings().create('custom_crm', listId, { sync_enabled: true });
await client.bindings().find(bindingId);
await client.bindings().update(bindingId, { sync_enabled: false });
await client.bindings().delete(bindingId);
await client.bindings().status(bindingId, 'completed', 0);await client.fieldMappings().config('custom_crm');
await client.fieldMappings().forBinding(bindingId).get();
await client.fieldMappings().forBinding(bindingId).update({ email: 'email', first_name: 'fname' });
await client.fieldMappings().forBinding(bindingId).reset();await client.autoTagging().rules('custom_crm');
await client.autoTagging().preview('custom_crm', { email: 'jane@example.com', plan: 'enterprise' });await client.analytics().dashboard();
await client.analytics().heatmap({ range: 60, email_type: 'all' });
await client.analytics().funnels();
await client.analytics().campaigns();
await client.analytics().forms();await client.sso().generate('owner@example.com'); // signed login URL
await client.sso().teamMembers();Successful calls resolve to the unwrapped data payload. Any error response
rejects with a typed error:
import { ApiError, TransportError } from '@emailfunnelai/sdk-node';
try {
await client.contacts().sync({ email: 'invalid' }).toList(listId);
} catch (error) {
if (error instanceof ApiError) {
error.status; // 422
error.errorType; // "validation_error"
error.messages; // { "contact.email": ["The email field is required."] }
error.retryAfter; // set on 429 rate limits
} else if (error instanceof TransportError) {
// network / timeout failure (no HTTP response)
}
}Both extend EmailFunnelSdkError, so you can catch that to handle any SDK
failure.
Branch on errorType for specific conditions — e.g. a contact list that has been
deactivated rejects new members with a 409 list_inactive:
try {
await client.contacts().sync(contact).toList(listId);
} catch (error) {
if (error instanceof ApiError && error.errorType === 'list_inactive') {
// The target list is inactive — reactivate it or pick another list.
}
}errorType |
Status | Meaning |
|---|---|---|
invalid_credentials |
401 | Missing/invalid project key or secret |
inactive_project |
403 | The connected project is inactive |
validation_error |
422 | Request body failed validation (messages set) |
invalid_email / suppressed |
422 | Email is undeliverable or suppressed |
list_inactive |
409 | Target contact list is inactive and rejects new members |
rate_limit_exceeded |
429 | 1000 req/hour cap hit (retryAfter set) |
sync_failed |
500 | Unexpected sync failure |
The client uses the global fetch by default. Provide your own HttpClient to
add logging, proxies, or a custom agent:
import { EmailFunnelAi, type HttpClient } from '@emailfunnelai/sdk-node';
const httpClient: HttpClient = {
async send(method, url, headers, body) {
// ...your transport...
return { status: 200, body: '{"data":{}}' };
},
};
const client = new EmailFunnelAi({ baseUrl, projectKey, projectSecret, httpClient });npm install
npm test # vitest, no network
npm run build # dual ESM + CJS + type declarationsTests mock HTTP and never touch the network. A coverage test asserts the SDK exposes every documented endpoint.
MIT © Email Funnel AI