Skip to content

Commit c52f854

Browse files
v7.0.0.beta.1
1 parent 4fd2ebc commit c52f854

19 files changed

Lines changed: 4594 additions & 2973 deletions

CHANGELOG.md

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,66 @@
11
# Changelog
22
All notable changes to this project will be documented in this file.
33

4-
## [7.0.0] - revision 2023-10-15
4+
## [7.0.0-beta.1] - revision 2023-10-15
55

66
### Added
77

8-
#### Support for returning list suppressions via the [/profiles endpoint](https://developers.klaviyo.com/en/reference/get_profiles)
8+
- OAuth Support
9+
- `OAuthApi` - Holds integration configuration and outlines how the SDK will interact with your application access and refresh token storage
10+
- `OAuthSession` - Makes API calls using OAuth and automatically updates `access tokens` when needed
11+
- `OAuthBasicSession` - Makes API calls with OAuth with no automatic updating
12+
13+
Learn how to use OAuth in the [README](README.md) and in the sample [application](https://github.com/klaviyo-labs/node-integration-example)
14+
15+
## [7.0.0] - revision 2023-10-15
16+
17+
### Added
918

10-
We now support filtering on list suppression with the get profiles endpoint, which brings us to parity with v2 list suppression endpoint that was the previously recommended solution.
19+
- Support for returning list suppressions via the [/profiles endpoint](https://developers.klaviyo.com/en/reference/get_profiles)
1120

12-
Rules for suppression [filtering](https://developers.klaviyo.com/en/docs/filtering_):
21+
Rules for suppression [filtering](https://developers.klaviyo.com/en/docs/filtering_):
1322

14-
- You may not mix-and-match list and global filters
15-
- You may only specify a single date filter
16-
- You may or may not specify a reason
17-
- You must specify a list_id to filter on any list suppression properties
23+
- You may not mix-and-match list and global filters
24+
- You may only specify a single date filter
25+
- You may or may not specify a reason
26+
- You must specify a list_id to filter on any list suppression properties
1827

19-
Examples:
28+
Examples:
2029

21-
```
22-
{filter: 'greater-than(subscriptions.email.marketing.suppression.timestamp,2023-03-01T01:00:00Z)'}
23-
{filter: 'greater-than(subscriptions.email.marketing.list_suppressions.timestamp,2023-03-01T01:00:00Z),equals(subscriptions.email.marketing.list_suppressions.list_id,”LIST_ID”')
24-
{filter: 'greater-than(subscriptions.email.marketing.suppression.timestamp,2023-03-01T01:00:00Z),equals(subscriptions.email.marketing.suppression.reason,"user_suppressed"')
25-
```
26-
##### Optionally retrieve subscription status on Get List Profiles, Get Segment Profiles, Get Event Profile
27-
Now you can retrieve subscription status on any endpoint that returns profiles, including `getListProfiles`, `getSegmentProfiles` and `getEventProfile`. Use `{additionalFieldsProfile: ['subscriptions']}` on these endpoints to include subscription information.
30+
```
31+
{filter: 'greater-than(subscriptions.email.marketing.suppression.timestamp,2023-03-01T01:00:00Z)'}
32+
{filter: 'greater-than(subscriptions.email.marketing.list_suppressions.timestamp,2023-03-01T01:00:00Z),equals(subscriptions.email.marketing.list_suppressions.list_id,”LIST_ID”')
33+
{filter: 'greater-than(subscriptions.email.marketing.suppression.timestamp,2023-03-01T01:00:00Z),equals(subscriptions.email.marketing.suppression.reason,"user_suppressed"')
34+
```
35+
- Optionally retrieve subscription status on:
36+
- `getListProfiles`
37+
- `getSegmentProfiles`
38+
- `getEventProfile`
39+
40+
Use `{additionalFieldsProfile: ['subscriptions']}` on these endpoints to include subscription information.
2841

29-
### Breaking changes
42+
### Changed
3043

31-
#### Subscription object not returned by default on Get Profile / Get Profiles
44+
- Subscription object not returned by default on Get Profile / Get Profiles
3245

33-
The subscription object is no longer returned by default with get profile(s) requests. However, it can be included by adding `?additional-fields[profile]=subscriptions` to the request. This change will allow us to provide a more performant experience when making requests to `GetProfiles` without including the subscriptions object.
46+
The subscription object is no longer returned by default with get profile(s) requests. However, it can be included by adding `?additional-fields[profile]=subscriptions` to the request. This change will allow us to provide a more performant experience when making requests to `GetProfiles` without including the subscriptions object.
3447

35-
#### Profile Subscription Fields Renamed
48+
- Profile Subscription Fields Renamed
3649

37-
In the interest of providing more clarity and information on the subscription object, we have renamed several fields, and added several as well. This will provide more context on a contact's subscriptions and consent, as well as boolean fields to see who you can or cannot message.
50+
In the interest of providing more clarity and information on the subscription object, we have renamed several fields, and added several as well. This will provide more context on a contact's subscriptions and consent, as well as boolean fields to see who you can or cannot message.
3851

39-
For SMSMarketing:
52+
For SMSMarketing:
4053

41-
- `timestamp` is now `consentTimestamp`
42-
- `lastUpdated` is a new field that mirrors `consenTimestamp`
43-
- `canReceiveSmsMarketing` is a new field which is `True` if the profile is consented for SMS
54+
- `timestamp` is now `consentTimestamp`
55+
- `lastUpdated` is a new field that mirrors `consenTimestamp`
56+
- `canReceiveSmsMarketing` is a new field which is `True` if the profile is consented for SMS
4457

45-
For EmailMarketing:
58+
For EmailMarketing:
4659

47-
- `timestamp` is now `consentTimestamp`
48-
- `canReceiveEmailMarketing` is True if the profile does not have a global suppression
49-
- `suppressions` is now `suppression`
50-
- `lastUpdated` is a new field that is the most recent of all the dates on the object
60+
- `timestamp` is now `consentTimestamp`
61+
- `canReceiveEmailMarketing` is True if the profile does not have a global suppression
62+
- `suppressions` is now `suppression`
63+
- `lastUpdated` is a new field that is the most recent of all the dates on the object
5164

5265
## [6.0.1] - revision 2023-09-15
5366
### Fixed

README.md

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Klaviyo Typescript SDK
22

3-
- SDK version: 7.0.0
3+
- SDK version: 7.0.0-beta.1
44

55
- Revision: 2023-10-15
66

@@ -47,7 +47,7 @@ This SDK is organized into the following resources:
4747

4848
You can install this library using `npm`.
4949

50-
`npm install [email protected]`
50+
`npm install [email protected]-beta.1`
5151

5252

5353
## source code
@@ -206,6 +206,170 @@ Profiles.getProfiles().then(result => {
206206
});
207207
```
208208

209+
## Using OAuth to connect to multiple Klaviyo accounts.
210+
211+
For users creating integrations or managing multiple Klaviyo accounts, Klaviyo's OAuth authentication will make these tasks easier.
212+
213+
### Getting started with OAuth
214+
215+
First, configure an integration. If you haven't set up an integration, learn about it in this [guide](https://help.klaviyo.com/hc/en-us/articles/18819413031067#h_01HACEKGF2DBDMGJ4JG6TV4AMN)
216+
217+
### Making API Calls with OAuth
218+
The `klaviyo-api` package can keep your `access token` up to date. If you have already developed a system for refreshing tokens or would like a more minimalist option, skip to [OAuthBasicSession](#oauthbasicsession)
219+
220+
#### TokenStorage
221+
For the OAuthApi to be storage agnostic, this interface must be implemented for the `OAuthApi` to retrieve and save you `access` and `refresh` tokens.
222+
Implement the `retrieve` and `save` functions outlined in the interface. If you need help getting started, check out the `storageHelpers.ts` in the [Klaviyo Example Typescript Integration](https://github.com/klaviyo-labs/node-integration-example)
223+
224+
Your implementation needs to include two methods:
225+
1. `save` is called after creating a new `access token` via the authorization flow or after refreshing the `access token`.
226+
Your code needs to update (and insert if you are going to be using `createTokens()`) the new `access` or `refresh` token information into storage
227+
to keep track of which of your integration users' access information you are referencing, the `customerIdentifer` is a unique value to help with lookup later.
228+
```typescript
229+
save(customerIdentifier: string, tokens: CreatedTokens): Promise<void> | void
230+
```
231+
2. `retrieve` leverages the `customerIdentifier` to look up the saved token information and returns it for the `OAuthApi` to use
232+
```typescript
233+
retrieve(customerIdentifier: string): Promise<RetrievedTokens> | RetrievedTokens
234+
```
235+
236+
```typescript
237+
import { TokenStorage } from 'klaviyo-api';
238+
class <Your Token Storage Class Name Here> implements TokenStorage
239+
```
240+
241+
#### OAuthApi
242+
This class holds the information about your specific integration. It takes three inputs:
243+
1. `clientId` - This is the id of your integration. Retrieve it from your integration's settings page
244+
2. `clientSecret` - This is the secret for your integration. The secret is generated upon the creation of your integration.
245+
3. `tokenStorage` - This is an instance of your implementation of `TokenStorage` and is called automatically when creating and refreshing `access tokens`
246+
247+
248+
```typescript
249+
import { OAuthApi } from 'klaviyo-api';
250+
251+
const oauthApi = new OAuthApi("<client id>", "<client secret>", <instance of your TokenStorage implimentation>)
252+
```
253+
254+
#### `OAuthSession`
255+
To make an API call, you need to create an `OAuthSession` instance. This session object is the OAuth equivalent of `ApiKeySession` and is used similarly.
256+
257+
It takes two properties
258+
1. `customerIdentifier` - This is how the session is going to grab a user's authentication information and let your implementation of `TokenStorage` know where to save any update `access token`
259+
2. `oauthApi` - This is the instance of `OAuthApi` created above. It will dictate how the session `saves` and `retrieves` the `access tokens`
260+
3. `retryOptions` - OPTIONAL - the `RetryOptions` instance outlines your desired exponential backoff retry options, outlined in [Retry Options](#retry-options) above
261+
262+
```typescript
263+
import { OAuthSession, ProfilesApi } from 'klaviyo-api';
264+
265+
const session = new OAuthSession(customerIdentifier, oauthApi)
266+
267+
//Pass the session into the API you want to use
268+
const profileApi = new ProfilesApi(session)
269+
```
270+
271+
#### `OAuthBasicSession`
272+
If you don't want to deal with any of the helpers above or don't want `klaviyo-api` to refresh your tokens for you, this is the alternative.
273+
274+
The `OAuthBasicSession` takes up to two parameters
275+
1. `accessToken` - The token is used in the API calls' authentication
276+
2. `retryOptions` - OPTIONAL - the `RetryOptions` instance outlines your desired exponential backoff retry options, outlined in [Retry Options](#retry-options) above
277+
278+
```typescript
279+
import { OAuthBasicSession } from 'klaviyo-api';
280+
281+
const session = new OAuthBasicSession("<access token>")
282+
283+
//Pass the session into the API you want to use
284+
const profileApi = new ProfilesApi(session)
285+
```
286+
287+
Remember to check for `401` errors. A 401 means that your token is probably expired.
288+
289+
#### `KlaviyoTokenError`
290+
291+
If an error occurred during an API call, check the error type with `isKlaviyoTokenError`. The name property will reflect which step the error occurred, reflecting whether it happened during creating, refreshing, saving, or retrieving the `name` tokens. The `cause` property will hold the error object of whatever specific error occurred.
292+
293+
### Authorization Flow
294+
295+
Build The authorization flow in the same application as with the rest of your integrations business logic or separately.
296+
There is no requirement that the authorization flow has to be backend and can be implemented entirely in a frontend application (in that case, you can ignore this section, as this repo shouldn't use this for frontend code)
297+
298+
To understand the authorization flow, there are two major resources to help:
299+
1. [OAuth authorization guide](https://help.klaviyo.com/hc/en-us/articles/18819413031067)
300+
2. [Node Integration Example](https://github.com/klaviyo-labs/node-integration-example)
301+
302+
If you implement your authorization flow on a node server, you can use these exposed helper functions.
303+
304+
#### OAuthApi
305+
306+
The OAuthApi class also exposes helpful Authorization flow utilities.
307+
308+
1. `generateAuthorizeUrl` - This helps correctly format the Klaviyo `/oauth/authorize` URL the application needs to redirect to so a user can approve your integration.
309+
1. `state` - This is the only way to identify which user just authorized your application (or failed to). `state` is passed back via query parameter to your `redirectUrl`.
310+
2. `scope` - The permissions the created `access tokens` will have. The user will be displayed these scopes during the authorization flow. For these permissions to work, also add them to your app settings in Klaviyo [here](www.klaviyo.com/oauth/client)
311+
3. `codeChallenge` - This is the value generated above by the `generateCode` function.
312+
4. `redirectUrl` - This is the URL that Klaviyo will redirect the user to once Authorization is completed (even if it is denied or has an error).
313+
Remember to whitelist this redirect URL in your integration's settings in Klaviyo.
314+
```typescript
315+
import { OAuthApi } from 'klaviyo-api'
316+
317+
const oauthApi = new OAuthApi("<client id>", "<client secret>", <TokenStorage implementation instance>)
318+
oauthApi.generateAuthorizeUrl(
319+
state, // It's suggested to use your internal identifier for the Klaviyo account that is authorizing
320+
scopes,
321+
codeChallenge,
322+
redirectUrl
323+
)
324+
```
325+
2. `createTokens` - Uses Klaviyo `/oauth/token/` endpoint to create `access` and `refresh` tokens
326+
1. `customerIdentifier` - This ID is NOT sent to Klaviyo's API. If the `/token` API call this method wraps is successful, the created tokens will be passed into your `save` method along with this `customerIdentifier` in your implementation of `TokenStorage`.
327+
2. `codeVerifier` - The verifier code must match the challenge code in the authorized URL redirect.
328+
3. `authorizationCode`- A User approving your integration creates this temporary authorization code. Your specified redirect URL receives this under a `code` query parameter.
329+
4. `redirectUrl` - The endpoint set in `generateAuthorizeUrl`. Whitelist the URL in your application settings.
330+
331+
```typescript
332+
import { OAuthApi } from 'klaviyo-api'
333+
334+
const oauthApi = new OAuthApi("<client id>", "<client secret>", <TokenStorage implementation instance>)
335+
await oauthApi.createTokens(
336+
customerIdentifier,
337+
codeVerifier,
338+
authorizationCode,
339+
redirectUrl
340+
)
341+
```
342+
3. `OAuthCallbackQueryParams` For typescript users, this object is an interface representing the possible query parameters sent to your redirect endpoint
343+
344+
345+
346+
#### Proof Key of Code Exchange (PKCE)
347+
348+
All the PKCE helper functions live within the `Pkce` namespace. Read about PKCE [here](https://help.klaviyo.com/hc/en-us/articles/18819413031067#h_01HACEKGF3AZ2KFGVPSSZNR5QW)
349+
350+
```typescript
351+
import { Pkce } from 'klaviyo-api'
352+
```
353+
354+
The `Pkce` namespace holds two different helper utilities
355+
1. `generateCodes` - This method will create the `codeVerifier` and `codeChallenge` needed later in the authorization flow.
356+
357+
```typescript
358+
import { Pkce } from 'klaviyo-api'
359+
360+
const pkceCodes = new Pkce.generateCodes()
361+
// the two codes can be accessed by
362+
const codeVerifier: string = pkceCodes.codeVerifier
363+
const codeChallenge: string = pkceCodes.codeChallenge
364+
```
365+
366+
2. `CodeStorage` - This is an OPTIONAL interface to help keep your code organized, to relate a `customerIdentifier` to their generated PKCE code
367+
368+
```typescript
369+
import { Pkce } from 'klaviyo-api'
370+
class <Your Code Storage Class Here> implements Pkce.CodeStorage
371+
```
372+
209373
## Optional Parameters and [JSON:API](https://jsonapi.org/) features
210374
211375
Here we will go over

api/accountsApi.ts

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,7 @@ export class AccountsApi {
3737
session: Session
3838

3939
protected _basePath = defaultBasePath;
40-
protected _defaultHeaders : any = {
41-
revision: "2023-10-15",
42-
"User-Agent": "klaviyo-api-node/7.0.0"
43-
};
40+
protected _defaultHeaders : any = {};
4441
protected _useQuerystring : boolean = false;
4542

4643
constructor(session: Session){
@@ -105,21 +102,27 @@ export class AccountsApi {
105102
params: localVarQueryParameters,
106103
}
107104

108-
this.session.applyToRequest(config)
109-
110-
return backOff<{ response: AxiosResponse; body: GetAccountResponse; }>( () => {
111-
return new Promise<{ response: AxiosResponse; body: GetAccountResponse; }>((resolve, reject) => {
112-
axios(config)
113-
.then(axiosResponse => {
114-
let body;
115-
body = ObjectSerializer.deserialize(axiosResponse.data, "GetAccountResponse");
116-
resolve({ response: axiosResponse, body: body });
117-
})
118-
.catch(error => {
119-
reject(error);
120-
})
121-
});
122-
}, this.session.getRetryOptions());
105+
await this.session.applyToRequest(config)
106+
107+
const request = async (config: AxiosRequestConfig, retried = false): Promise<{ response: AxiosResponse; body: GetAccountResponse; }> => {
108+
try {
109+
const axiosResponse = await axios(config)
110+
let body;
111+
body = ObjectSerializer.deserialize(axiosResponse.data, "GetAccountResponse");
112+
return ({response: axiosResponse, body: body});
113+
} catch (error) {
114+
if (await this.session.refreshAndRetry(error, retried)) {
115+
await this.session.applyToRequest(config)
116+
return request(config, true)
117+
}
118+
throw error
119+
}
120+
}
121+
122+
return backOff<{ response: AxiosResponse; body: GetAccountResponse; }>(
123+
() => {return request(config)},
124+
this.session.getRetryOptions()
125+
);
123126
}
124127
/**
125128
* Retrieve the account(s) associated with a given private API key. This will return 1 account object within the array. You can use this to retrieve account-specific data (contact information, timezone, currency, Public API key, etc.) or test if a Private API Key belongs to the correct account prior to performing subsequent actions with the API.<br><br>*Rate limits*:<br>Burst: `1/s`<br>Steady: `15/m` **Scopes:** `accounts:read`
@@ -153,20 +156,26 @@ export class AccountsApi {
153156
params: localVarQueryParameters,
154157
}
155158

156-
this.session.applyToRequest(config)
157-
158-
return backOff<{ response: AxiosResponse; body: GetAccountResponseCollection; }>( () => {
159-
return new Promise<{ response: AxiosResponse; body: GetAccountResponseCollection; }>((resolve, reject) => {
160-
axios(config)
161-
.then(axiosResponse => {
162-
let body;
163-
body = ObjectSerializer.deserialize(axiosResponse.data, "GetAccountResponseCollection");
164-
resolve({ response: axiosResponse, body: body });
165-
})
166-
.catch(error => {
167-
reject(error);
168-
})
169-
});
170-
}, this.session.getRetryOptions());
159+
await this.session.applyToRequest(config)
160+
161+
const request = async (config: AxiosRequestConfig, retried = false): Promise<{ response: AxiosResponse; body: GetAccountResponseCollection; }> => {
162+
try {
163+
const axiosResponse = await axios(config)
164+
let body;
165+
body = ObjectSerializer.deserialize(axiosResponse.data, "GetAccountResponseCollection");
166+
return ({response: axiosResponse, body: body});
167+
} catch (error) {
168+
if (await this.session.refreshAndRetry(error, retried)) {
169+
await this.session.applyToRequest(config)
170+
return request(config, true)
171+
}
172+
throw error
173+
}
174+
}
175+
176+
return backOff<{ response: AxiosResponse; body: GetAccountResponseCollection; }>(
177+
() => {return request(config)},
178+
this.session.getRetryOptions()
179+
);
171180
}
172181
}

0 commit comments

Comments
 (0)