diff --git a/docs/app.md b/docs/app.md index 082f653..84c5128 100644 --- a/docs/app.md +++ b/docs/app.md @@ -1863,3 +1863,364 @@ api.updateCollectionContent(9, [ - **collectionId**: The collection's numeric id (required) - **content**: An array of row objects (required) + +## Deliverability + +### ESP suppressions + +Manage the email-provider suppression lists. `suppressionType` is one of `"blocks"`, `"bounces"`, `"spam_reports"`, or `"invalid_emails"`. + +### api.searchSuppression(email) + +Search every suppression category for an email address. + +```javascript +api.searchSuppression("person@example.com"); +``` + +#### Options + +- **email**: The email address to search for (required) + +### api.getSuppressions(suppressionType, options) + +List suppressions in a category (offset-based pagination). + +```javascript +api.getSuppressions("bounces", { limit: 50, offset: 0 }); +``` + +#### Options + +- **suppressionType**: One of `"blocks"` / `"bounces"` / `"spam_reports"` / `"invalid_emails"` (required) +- **options**: Object (optional) — `limit` (1–1000, default 100), `offset` (default 0), `email`, `domain` + +### api.getDomainSuppressions(domainName, suppressionType, options) + +List suppressions in a category for a single sending domain (cursor-based pagination — preferred for large volumes). + +```javascript +api.getDomainSuppressions("mail.example.com", "bounces", { limit: 100, start: cursor }); +``` + +#### Options + +- **domainName**: The sending domain (required) +- **suppressionType**: One of `"blocks"` / `"bounces"` / `"spam_reports"` / `"invalid_emails"` (required) +- **options**: Object (optional) — `limit` (1–1000, default 100), `email`, `start` (pagination cursor from a previous page's `next`) + +### api.createSuppression(suppressionType, email) + +Add an email address to a suppression category. + +```javascript +api.createSuppression("bounces", "person@example.com"); +``` + +#### Options + +- **suppressionType**: One of `"blocks"` / `"bounces"` / `"spam_reports"` / `"invalid_emails"` (required) +- **email**: The email address to suppress (required) + +### api.deleteSuppression(suppressionType, email) + +Remove an email address from a suppression category. Returns no content on success. + +```javascript +api.deleteSuppression("bounces", "person@example.com"); +``` + +#### Options + +- **suppressionType**: One of `"blocks"` / `"bounces"` / `"spam_reports"` / `"invalid_emails"` (required) +- **email**: The email address to unsuppress (required) + +### Reporting webhooks + +Manage [reporting webhooks](https://customer.io/docs/journeys/webhooks/) that POST message events to your endpoint. Webhook ids are **integers**. + +### api.listReportingWebhooks() + +List the reporting webhooks in your workspace. + +```javascript +api.listReportingWebhooks(); +``` + +### api.createReportingWebhook(webhook) + +Create a reporting webhook. + +```javascript +api.createReportingWebhook({ + endpoint: "https://example.com/cio-events", + events: ["sent", "delivered", "opened", "clicked", "bounced"], + name: "Production events", + with_content: false, +}); +``` + +#### Options + +- **webhook**: The webhook definition + - _endpoint_: The destination URL that events are POSTed to (required) + - _events_: The event types to send (e.g. `drafted`, `sent`, `delivered`, `opened`, `clicked`, `bounced`, `converted`) + - _name_: Display name, ≤190 characters + - _full_resolution_: Send an event for every occurrence rather than de-duplicating + - _with_content_: Include message content in the payloads + - _disabled_: Create the webhook in a disabled state + +### api.getReportingWebhook(webhookId) + +Get a single reporting webhook. + +```javascript +api.getReportingWebhook(4); +``` + +#### Options + +- **webhookId**: The webhook's numeric id (required) + +### api.updateReportingWebhook(webhookId, updates) + +Update a reporting webhook. Any subset of fields may be provided. + +```javascript +api.updateReportingWebhook(4, { disabled: true }); +``` + +#### Options + +- **webhookId**: The webhook's numeric id (required) +- **updates**: Object with any of `endpoint`, `events`, `name`, `full_resolution`, `with_content`, `disabled` + +### api.deleteReportingWebhook(webhookId) + +Delete a reporting webhook. Returns no content on success. + +```javascript +api.deleteReportingWebhook(4); +``` + +#### Options + +- **webhookId**: The webhook's numeric id (required) + +## Content & sender utilities + +### Snippets + +Manage [snippets](https://customer.io/docs/journeys/snippets/) — reusable content blocks referenced from messages. Snippets are identified by `name`. + +### api.getSnippets() + +List the snippets in your workspace. + +```javascript +api.getSnippets(); +``` + +### api.createSnippet(snippet) + +Create a snippet. + +```javascript +api.createSnippet({ name: "footer", value: "
© 2026 Example
" }); +``` + +#### Options + +- **snippet**: The snippet definition + - _name_: The snippet's unique name/key (required) + - _value_: The snippet's value; may contain Liquid (required) + +### api.updateSnippet(snippet) + +Create or update a snippet (upsert by name). + +```javascript +api.updateSnippet({ name: "footer", value: "© 2027 Example
" }); +``` + +#### Options + +- **snippet**: The snippet definition — `name` and `value` (both required) + +### api.deleteSnippet(name) + +Delete a snippet by name. Returns no content on success. Fails if the snippet is still in use. + +```javascript +api.deleteSnippet("footer"); +``` + +#### Options + +- **name**: The snippet's name (required) + +### Sender identities + +### api.getSenderIdentities(options) + +List the sender identities in your workspace. + +```javascript +api.getSenderIdentities({ limit: 50, sort: "asc" }); +``` + +#### Options + +- **options**: Object (optional) — `start`, `limit`, `sort` ("asc" / "desc"), `hidden` (omit for all, `true`/`false` to filter) + +### api.getSenderIdentity(senderId) + +Get a single sender identity. + +```javascript +api.getSenderIdentity(12); +``` + +#### Options + +- **senderId**: The sender identity's numeric id (required) + +### api.getSenderIdentityUsedBy(senderId) + +List the campaigns and newsletters that use a sender identity. + +```javascript +api.getSenderIdentityUsedBy(12); +``` + +#### Options + +- **senderId**: The sender identity's numeric id (required) + +### Messages + +Look up sent messages (deliveries) across your workspace. For a single person's messages, use [`getCustomerMessages`](#apigetcustomermessagescustomerid-options). + +### api.getMessages(options) + +List sent messages. + +```javascript +api.getMessages({ metric: "delivered", type: "email", limit: 50 }); +``` + +#### Options + +- **options**: Object (optional) + - _start_: Pagination cursor from a previous page's `next` + - _limit_: Maximum number of results + - _drafts_: Return only drafts (`true`) or exclude them (default) + - _metric_: Filter to a delivery metric (e.g. `delivered`, `opened`, `bounced`) + - _type_: Scope to a channel ("email" / "webhook" / "twilio" / "whatsapp" / "slack" / "push" / "in_app") + - _campaign_id_ / _action_id_ / _newsletter_id_ / _transactional_id_ / _trigger_id_ / _template_id_ / _content_id_: Scope to a single resource + - _start_ts_ / _end_ts_: Unix timestamp bounds (seconds) + - _associations_: Include related campaigns/actions/newsletters/contents + - _get_tracked_responses_: Include tracked responses on each delivery + +### api.getMessage(messageId, options) + +Get a single sent message. + +```javascript +api.getMessage("RPCImftJDcAAAAd...", { archived_message: true }); +``` + +#### Options + +- **messageId**: The delivery id (`CIO-Delivery-ID`) (required) +- **options**: Object (optional) — `archived_message`, `associations`, `get_tracked_responses` + +### api.getArchivedMessage(messageId) + +Get the archived content of a single sent message. + +```javascript +api.getArchivedMessage("RPCImftJDcAAAAd..."); +``` + +#### Options + +- **messageId**: The delivery id (`CIO-Delivery-ID`) (required) + +## Imports, data index & workspace info + +### api.createImport(importData) + +Start a [CSV import](https://customer.io/docs/api/app/#operation/createImports). The CSV is loaded from a hosted URL you provide as `data_file_url`. + +```javascript +api.createImport({ + data_file_url: "https://example.com/people.csv", + type: "people", + identifier: "email", + name: "Q3 signups", +}); +``` + +#### Options + +- **importData**: The import definition + - _data_file_url_: URL of the CSV file to import (required) + - _type_: `"people"` / `"event"` / `"object"` / `"relationship"` (required) + - _identifier_: Which column keys the rows — `"id"`/`"email"` for people & events; `"id"`/`"email"`/`"cio_id"` for relationships + - _object_type_id_: Required when `type` is `"object"` + - _name_: Display name (defaults to the filename) + - _description_: Description of the import + - _people_to_process_ / _data_to_process_: `"all"` / `"only_existing"` / `"only_new"` (mutually exclusive) + +### api.getImport(importId) + +Get the status of an import. + +```javascript +api.getImport(15); +``` + +#### Options + +- **importId**: The import's numeric id (required) + +### api.batchUpdateAttributes(attributes) + +Batch-update attribute metadata (descriptions, etc.) — up to 100 at a time. + +```javascript +api.batchUpdateAttributes([{ name: "plan", description: "Subscription plan" }]); +``` + +#### Options + +- **attributes**: A non-empty array of attribute updates (max 100). Each requires a `name`; may also include `description`, `object_type_id`, `is_relationship`, `event_name`, `privacy_level` + +### api.batchUpdateEvents(events) + +Batch-update event metadata — up to 100 at a time. + +```javascript +api.batchUpdateEvents([{ name: "purchase", description: "Completed a purchase" }]); +``` + +#### Options + +- **events**: A non-empty array of event updates (max 100). Each requires a `name`; may also include `description` + +### api.listWorkspaces() + +List the workspaces (environments) in your account, with usage counts. + +```javascript +api.listWorkspaces(); +``` + +### api.getIpAddresses() + +List the Customer.io egress IP addresses (for allowlisting). + +```javascript +api.getIpAddresses(); +``` diff --git a/lib/api.ts b/lib/api.ts index abe2b4c..777f2f7 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -519,6 +519,165 @@ export type CollectionUpdate = { url?: string; }; +/** An email-suppression category for the ESP (deliverability) endpoints. */ +export type SuppressionType = 'blocks' | 'bounces' | 'spam_reports' | 'invalid_emails'; + +/** Options for {@link APIClient.getSuppressions} (offset-based). */ +export type SuppressionsOptions = { + /** Page size, 1–1000. Defaults to 100. */ + limit?: number; + /** Number of records to skip. Defaults to 0. */ + offset?: number; + /** Filter to a single email address. */ + email?: string; + /** Filter to a single sending domain. */ + domain?: string; +}; + +/** Options for {@link APIClient.getDomainSuppressions} (cursor-based). */ +export type DomainSuppressionsOptions = { + /** Page size, 1–1000. Defaults to 100. */ + limit?: number; + /** Filter to a single email address. */ + email?: string; + /** Pagination cursor returned as `next` by a previous page. */ + start?: string; +}; + +/** Definition for creating a reporting webhook via {@link APIClient.createReportingWebhook}. */ +export type ReportingWebhookInput = { + /** The destination URL that events are POSTed to (required). */ + endpoint: string; + /** The event types to send (e.g. `drafted`, `sent`, `delivered`, `opened`, `clicked`, `bounced`). */ + events: string[]; + /** Display name (≤190 characters). */ + name?: string; + /** When `true`, send an event for every occurrence rather than de-duplicating. */ + full_resolution?: boolean; + /** When `true`, include message content in the event payloads. */ + with_content?: boolean; + /** When `true`, create the webhook in a disabled state. */ + disabled?: boolean; +}; + +/** Fields for updating a reporting webhook via {@link APIClient.updateReportingWebhook}. Any subset may be provided. */ +export type ReportingWebhookUpdate = { + endpoint?: string; + events?: string[]; + name?: string; + full_resolution?: boolean; + with_content?: boolean; + disabled?: boolean; +}; + +/** A snippet definition for {@link APIClient.createSnippet} / {@link APIClient.updateSnippet}. */ +export type SnippetInput = { + /** The snippet's unique name/key. */ + name: string; + /** The snippet's value (may contain Liquid). */ + value: string; +}; + +/** Options for {@link APIClient.getSenderIdentities}. */ +export type SenderIdentitiesOptions = PaginationOptions & { + /** Sort direction. Defaults to `asc`. */ + sort?: SortDirection; + /** Filter by hidden status. Omit to return all; `true`/`false` to filter. */ + hidden?: boolean; +}; + +/** Options for {@link APIClient.getMessages}. */ +export type MessagesOptions = PaginationOptions & { + /** Return only drafts (`true`) or exclude them (default). */ + drafts?: boolean; + /** Filter to deliveries with this metric (e.g. `delivered`, `opened`, `bounced`). */ + metric?: string; + /** Scope to a single channel. */ + type?: MetricType; + /** Scope to a single campaign. */ + campaign_id?: string | number; + /** Scope to a single campaign action. */ + action_id?: string | number; + /** Scope to a single newsletter. */ + newsletter_id?: string | number; + /** Scope to a single transactional message. */ + transactional_id?: string | number; + /** Scope to a single broadcast trigger (requires `campaign_id`). */ + trigger_id?: string | number; + /** Scope to a single template. */ + template_id?: string | number; + /** Scope to a single content id. */ + content_id?: string | number; + /** Only include deliveries after this Unix timestamp (seconds). */ + start_ts?: number; + /** Only include deliveries before this Unix timestamp (seconds). */ + end_ts?: number; + /** Include the related campaigns/actions/newsletters/contents in the response. */ + associations?: boolean; + /** Include tracked responses on each delivery. */ + get_tracked_responses?: boolean; +}; + +/** Options for {@link APIClient.getMessage}. */ +export type MessageOptions = { + /** Include the archived message content (rate-limited). */ + archived_message?: boolean; + /** Include the related campaign/action/newsletter/content in the response. */ + associations?: boolean; + /** Include tracked responses on the delivery. */ + get_tracked_responses?: boolean; +}; + +/** What a CSV import loads. */ +export type ImportType = 'people' | 'event' | 'object' | 'relationship'; + +/** Which records an import processes. */ +export type ImportProcessScope = 'all' | 'only_existing' | 'only_new'; + +/** Definition for creating a CSV import via {@link APIClient.createImport}. */ +export type ImportInput = { + /** URL of the CSV file to import (required). */ + data_file_url: string; + /** What the CSV loads (required). */ + type: ImportType; + /** Which identifier the CSV keys rows by. `id`/`email` for people & events; `id`/`email`/`cio_id` for relationships. */ + identifier?: 'id' | 'email' | 'cio_id'; + /** Object type id — required when `type` is `object`. */ + object_type_id?: string | number; + /** Display name (defaults to the filename). */ + name?: string; + /** Description of the import. */ + description?: string; + /** For people imports: which profiles to process. Mutually exclusive with `data_to_process`. */ + people_to_process?: ImportProcessScope; + /** For object/relationship imports: which records to process. Mutually exclusive with `people_to_process`. */ + data_to_process?: ImportProcessScope; +}; + +/** A single attribute-metadata update for {@link APIClient.batchUpdateAttributes}. */ +export type DataIndexAttribute = { + /** The attribute name (required). */ + name: string; + /** A human-readable description. */ + description?: string; + /** Scope the attribute to an object type. */ + object_type_id?: number; + /** Whether the attribute is a relationship attribute. */ + is_relationship?: boolean; + /** Scope the attribute to an event. */ + event_name?: string; + /** Privacy level (requires the sensitive-attributes feature). */ + privacy_level?: number; +}; + +/** A single event-metadata update for {@link APIClient.batchUpdateEvents}. */ +export type DataIndexEvent = { + /** The event name (required). */ + name: string; + /** A human-readable description. */ + description?: string; +}; + type APIDefaults = RequestDefaults & { region: Region; url?: string; retry?: Partial© 2026
' }; + t.context.client.createSnippet(snippet); + t.true(post.calledWith(`${API}/snippets`, snippet)); +}); + +test('#updateSnippet: puts the snippet body (upsert) and validates', (t) => { + const put = sinon.stub(t.context.client.request, 'put'); + t.throws(() => (t.context.client.updateSnippet as any)(null), { message: 'snippet is required' }); + t.throws(() => (t.context.client.updateSnippet as any)({ value: 'v' }), { message: 'snippet.name is required' }); + t.throws(() => (t.context.client.updateSnippet as any)({ name: 'n' }), { message: 'snippet.value is required' }); + const snippet = { name: 'footer', value: 'updated
' }; + t.context.client.updateSnippet(snippet); + t.true(put.calledWith(`${API}/snippets`, snippet)); +}); + +test('#deleteSnippet: deletes a snippet by name and validates', (t) => { + const destroy = sinon.stub(t.context.client.request, 'destroy'); + t.throws(() => t.context.client.deleteSnippet(''), { message: 'name is required' }); + t.context.client.deleteSnippet('foo bar'); + t.true(destroy.calledWith(`${API}/snippets/foo%20bar`)); +}); + +// Sender identities + +test('#getSenderIdentities: gets the endpoint with sort/pagination/hidden', (t) => { + const get = sinon.stub(t.context.client.request, 'get'); + t.context.client.getSenderIdentities(); + t.true(get.calledWith(`${API}/sender_identities`)); + t.context.client.getSenderIdentities({ start: 'cur', limit: 10, sort: 'desc', hidden: false }); + t.true(get.calledWith(`${API}/sender_identities?start=cur&limit=10&sort=desc&hidden=false`)); +}); + +test('#getSenderIdentity: gets a single identity and validates', (t) => { + const get = sinon.stub(t.context.client.request, 'get'); + t.throws(() => t.context.client.getSenderIdentity(''), { message: 'senderId is required' }); + t.context.client.getSenderIdentity(12); + t.true(get.calledWith(`${API}/sender_identities/12`)); +}); + +test('#getSenderIdentityUsedBy: gets the used_by endpoint and validates', (t) => { + const get = sinon.stub(t.context.client.request, 'get'); + t.throws(() => t.context.client.getSenderIdentityUsedBy(''), { message: 'senderId is required' }); + t.context.client.getSenderIdentityUsedBy(12); + t.true(get.calledWith(`${API}/sender_identities/12/used_by`)); +}); + +// Messages + +test('#getMessages: gets the messages endpoint with no query by default', (t) => { + const get = sinon.stub(t.context.client.request, 'get'); + t.context.client.getMessages(); + t.true(get.calledWith(`${API}/messages`)); +}); + +test('#getMessages: forwards filters and pagination', (t) => { + const get = sinon.stub(t.context.client.request, 'get'); + t.context.client.getMessages({ + start: 'cur', + limit: 20, + drafts: false, + metric: 'delivered', + type: 'email', + campaign_id: 3, + action_id: 4, + newsletter_id: 5, + transactional_id: 6, + trigger_id: 7, + template_id: 8, + content_id: 9, + start_ts: 100, + end_ts: 200, + associations: true, + get_tracked_responses: true, + }); + t.true( + get.calledWith( + `${API}/messages?start=cur&limit=20&drafts=false&metric=delivered&type=email&campaign_id=3&action_id=4&newsletter_id=5&transactional_id=6&trigger_id=7&template_id=8&content_id=9&start_ts=100&end_ts=200&associations=true&get_tracked_responses=true`, + ), + ); +}); + +test('#getMessage: gets a single message with expansions and validates', (t) => { + const get = sinon.stub(t.context.client.request, 'get'); + t.throws(() => t.context.client.getMessage(''), { message: 'messageId is required' }); + t.context.client.getMessage('abc-123'); + t.true(get.calledWith(`${API}/messages/abc-123`)); + t.context.client.getMessage('abc-123', { archived_message: true, associations: true }); + t.true(get.calledWith(`${API}/messages/abc-123?archived_message=true&associations=true`)); +}); + +test('#getArchivedMessage: gets the archived_message endpoint and validates', (t) => { + const get = sinon.stub(t.context.client.request, 'get'); + t.throws(() => t.context.client.getArchivedMessage(''), { message: 'messageId is required' }); + t.context.client.getArchivedMessage('abc-123'); + t.true(get.calledWith(`${API}/messages/abc-123/archived_message`)); +}); + +// Imports + +test('#createImport: posts the wrapped import body and validates', (t) => { + const post = sinon.stub(t.context.client.request, 'post'); + t.throws(() => (t.context.client.createImport as any)(null), { message: 'importData is required' }); + t.throws(() => (t.context.client.createImport as any)({ type: 'people' }), { + message: 'importData.data_file_url is required', + }); + t.throws(() => (t.context.client.createImport as any)({ data_file_url: 'https://x/f.csv' }), { + message: 'importData.type is required', + }); + const imp = { + data_file_url: 'https://example.com/people.csv', + type: 'people' as const, + identifier: 'email' as const, + }; + t.context.client.createImport(imp); + t.true(post.calledWith(`${API}/imports`, { import: imp })); +}); + +test('#getImport: gets a single import and validates', (t) => { + const get = sinon.stub(t.context.client.request, 'get'); + t.throws(() => t.context.client.getImport(''), { message: 'importId is required' }); + t.context.client.getImport(15); + t.true(get.calledWith(`${API}/imports/15`)); +}); + +// Data index + +test('#batchUpdateAttributes: posts the wrapped attributes and validates', (t) => { + const post = sinon.stub(t.context.client.request, 'post'); + t.throws(() => (t.context.client.batchUpdateAttributes as any)(null), { message: 'attributes is required' }); + t.throws(() => t.context.client.batchUpdateAttributes([]), { message: 'attributes is required' }); + const attributes = [{ name: 'plan', description: 'Subscription plan' }]; + t.context.client.batchUpdateAttributes(attributes); + t.true(post.calledWith(`${API}/data_index/attributes`, { attributes })); +}); + +test('#batchUpdateEvents: posts the wrapped events and validates', (t) => { + const post = sinon.stub(t.context.client.request, 'post'); + t.throws(() => (t.context.client.batchUpdateEvents as any)(null), { message: 'events is required' }); + t.throws(() => t.context.client.batchUpdateEvents([]), { message: 'events is required' }); + const events = [{ name: 'purchase', description: 'Completed a purchase' }]; + t.context.client.batchUpdateEvents(events); + t.true(post.calledWith(`${API}/data_index/events`, { events })); +}); + +// Misc + +test('#listWorkspaces: gets the workspaces endpoint', (t) => { + const get = sinon.stub(t.context.client.request, 'get'); + t.context.client.listWorkspaces(); + t.true(get.calledWith(`${API}/workspaces`)); +}); + +test('#getIpAddresses: gets the ip_addresses endpoint', (t) => { + const get = sinon.stub(t.context.client.request, 'get'); + t.context.client.getIpAddresses(); + t.true(get.calledWith(`${API}/info/ip_addresses`)); +}); diff --git a/test/integration/live.env.example b/test/integration/live.env.example index dfee026..17031e2 100644 --- a/test/integration/live.env.example +++ b/test/integration/live.env.example @@ -52,3 +52,5 @@ CIO_TEST_DELIVERY_ID= # Object type id (+ optional object id) for object attribute/relationship/find reads CIO_TEST_OBJECT_TYPE_ID= CIO_TEST_OBJECT_ID= +# Downloadable CSV URL for createImport +CIO_TEST_IMPORT_CSV_URL= diff --git a/test/integration/live.ts b/test/integration/live.ts index 216fc51..e201f61 100644 --- a/test/integration/live.ts +++ b/test/integration/live.ts @@ -28,6 +28,7 @@ * CIO_TEST_OBJECT_ID object id for object reads (defaults to a throwaway id) * CIO_TEST_CAMPAIGN_ID campaign id for campaign read methods * CIO_TEST_TRIGGER_ID trigger id (with CIO_TEST_BROADCAST_ID) for trigger status/errors + * CIO_TEST_IMPORT_CSV_URL downloadable CSV URL for createImport * * Profile lifecycle: one throwaway customer per run, id = `sdk-live-${uuid()}`. * Cleanup is best-effort; the dedicated workspace tolerates orphans. @@ -447,6 +448,100 @@ liveTest('collection create -> read -> content -> delete round-trip', async (t) t.pass(); }); +liveTest('esp suppression reads resolve', async (t) => { + const list = (await api!.getSuppressions('bounces', { limit: 1 })) as { suppressions?: unknown }; + t.true('suppressions' in list); + await api!.searchSuppression(`absent-${randomUUID()}@example.com`).catch(() => undefined); + t.pass(); +}); + +liveTest('esp suppression create -> delete round-trip', async (t) => { + const email = `sdk-live-${randomUUID()}@example.com`; + await api!.createSuppression('bounces', email).catch(() => undefined); + await api!.deleteSuppression('bounces', email).catch(() => undefined); + t.pass(); +}); + +liveTest('listReportingWebhooks resolves', async (t) => { + const result = (await api!.listReportingWebhooks()) as { reporting_webhooks?: unknown }; + t.true('reporting_webhooks' in result); +}); + +liveTest('reporting webhook create -> read -> update -> delete round-trip', async (t) => { + const created = (await api! + .createReportingWebhook({ + endpoint: `https://example.com/sdk-live-${customerId}`, + events: ['sent', 'delivered'], + name: `sdk-live-${customerId}`, + disabled: true, + }) + .catch(() => undefined)) as { id?: number } | undefined; + const webhookId = created?.id; + + if (webhookId !== undefined) { + await api!.getReportingWebhook(webhookId).catch(() => undefined); + await api!.updateReportingWebhook(webhookId, { name: `sdk-live-${customerId}-renamed` }).catch(() => undefined); + await api!.deleteReportingWebhook(webhookId).catch(() => undefined); + } + + t.pass(); +}); + +liveTest('snippet upsert -> read -> delete round-trip', async (t) => { + const name = `sdk-live-${customerId}`; + await api!.updateSnippet({ name, value: 'hello' }).catch(() => undefined); + const list = (await api!.getSnippets()) as { snippets?: unknown }; + t.true('snippets' in list); + await api!.deleteSnippet(name).catch(() => undefined); + t.pass(); +}); + +liveTest('sender identity reads resolve', async (t) => { + const result = (await api!.getSenderIdentities({ limit: 5 })) as { sender_identities?: unknown }; + t.true('sender_identities' in result); +}); + +liveTest('getMessages resolves', async (t) => { + const result = (await api!.getMessages({ limit: 5 })) as { messages?: unknown }; + t.true('messages' in result); +}); + +liveTest('listWorkspaces resolves', async (t) => { + const result = (await api!.listWorkspaces()) as { workspaces?: unknown }; + t.true('workspaces' in result); +}); + +liveTest('getIpAddresses resolves', async (t) => { + const result = (await api!.getIpAddresses()) as { ip_addresses?: unknown }; + t.true('ip_addresses' in result); +}); + +liveTest('data_index batch updates resolve', async (t) => { + await api! + .batchUpdateAttributes([{ name: `sdk_live_${customerId}`, description: 'sdk live test' }]) + .catch(() => undefined); + await api! + .batchUpdateEvents([{ name: `sdk_live_${customerId}`, description: 'sdk live test' }]) + .catch(() => undefined); + t.pass(); +}); + +needs('CIO_TEST_IMPORT_CSV_URL')('import create -> get round-trip', async (t) => { + const created = (await api! + .createImport({ + data_file_url: process.env.CIO_TEST_IMPORT_CSV_URL!, + type: 'people', + identifier: 'email', + name: `sdk-live-${customerId}`, + }) + .catch(() => undefined)) as { import?: { id?: number } } | undefined; + const importId = created?.import?.id; + if (importId !== undefined) { + await api!.getImport(importId).catch(() => undefined); + } + t.pass(); +}); + liveTest('track records an event on the profile', async (t) => { await track!.track(customerId, { name: 'sdk_live_event', data: { run: customerId } }); t.pass();