diff --git a/README.md b/README.md index 505a63d..5bb51a7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Ballerina HubSpot Marketing Campaigns connector +# Ballerina HubSpot Marketing Campaigns Connector [![Build](https://github.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/actions/workflows/ci.yml/badge.svg)](https://github.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/actions/workflows/ci.yml) [![Trivy](https://github.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/actions/workflows/trivy-scan.yml/badge.svg)](https://github.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/actions/workflows/trivy-scan.yml) @@ -8,21 +8,184 @@ ## Overview -[//]: # (TODO: Add overview mentioning the purpose of the module, supported REST API versions, and other high-level details.) +[HubSpot](https://www.hubspot.com) is an AI-powered customer relationship management (CRM) platform. + +The `ballerinax/module-ballerinax-hubspot.marketing.campaigns` connector offers APIs to connect and interact with the [Hubspot Marketing Campaigns API](https://developers.hubspot.com/docs/guides/api/marketing/campaigns) endpoints, specifically based on the [HubSpot REST API](https://developers.hubspot.com/docs/reference/api/overview) ## Setup guide -[//]: # (TODO: Add detailed steps to obtain credentials and configure the module.) +To use the HubSpot Marketing Campaigns connector, you must have access to the HubSpot API through a HubSpot developer account and a HubSpot App under it. Therefore you need to register for a developer account at HubSpot if you don't have one already. + +### Step 1: Create/Login to a HubSpot Developer Account + +If you have an account already, go to the [HubSpot developer portal](https://developers.hubspot.com/) + +If you don't have a HubSpot Developer Account you can sign up to a free account [here](https://developers.hubspot.com/get-started) + +### Step 2 (Optional): Create a Developer Test Account +Within app developer accounts, you can create a [developer test account](https://developers.hubspot.com/docs/getting-started/account-types#developer-test-accounts) under your account to test apps and integrations without affecting any real HubSpot data. + +> **Note**: These accounts are only for development and testing purposes. In production you should not use Developer Test Accounts. + + 1. Go to Test accounts section from the left sidebar. + +![Test Account](docs/resources/testAccount.png) + + 2. Click on the Create developer test account button on the top right corner. + +![Development Test Account](docs/resources/developmentTestAccount.png) + + 3. In the pop-up window, provide a name for the test account and click on the Create button. + +![Create Account](docs/resources/createAccount.png) + + 4. You will see the newly created test account in the list of test accounts. + +![Test Account Portal](docs/resources/testAccountPortal.png) + +### Step 3: Create a HubSpot App + + 1. Navigate to the Apps section from the left sidebar and click on the Create app button on the top right corner. + +![App Section](docs/resources/appSection.png) + + 2. Provide a public app name and description for your app. + +![Naming the App](docs/resources/namingApp.png) + +### Step 4: Setup Authentication + + 1. Move to the Auth tab. + +![Auth Section](docs/resources/auth.png) + + 2. In the Scopes section, add the following scopes for your app using the Add new scopes button. + - `marketing.campaigns.read` + - `marketing.campaigns.revenue.read` + - `marketing.campaigns.write` + +![Marketing Scopes](docs/resources/marketingScopes.png) + + 3. In the Redirect URL section, add the redirect URL for your app. This is the URL where the user will be redirected after the authentication process. You can use `localhost` for testing purposes. Then click the "Create App" button. + +![Redirect URL](docs/resources/redirectURL.png) + +### Step 5: Get the Client ID and Client Secret + +Navigate to the Auth tab and you will see the `Client ID`, and `Client Secret` for your app. Make sure to save these values. + +![Client ID and Client Secret](docs/resources/clientId_secretId.png) + +### Step 6: Setup Authentication Flow + +Before proceeding with the Quickstart, ensure you have obtained the Access Token using the following steps: + + 1. Create an authorization URL using the following format: + + ``` + https://app.hubspot.com/oauth/authorize?client_id=&scope=&redirect_uri= + ``` + Replace the ``, ``, and `` with your specific value. + + 2. Paste it in the browser and select your developer test account to intall the app when prompted. + + ![Account Select](docs/resources/accountSelect.png) + + 3. A code will be displayed in the browser. Copy the code. + + 4. Run the following curl command. Replace the ``, ``, and `` with your specific value. Use the code you received in the above step 3 as the CODE below + + - Linux/macOS + ```bash + curl --request POST \ +--url https://api.hubapi.com/oauth/v1/token \ +--header 'content-type: application/x-www-form-urlencoded' \ +--data 'grant_type=authorization_code&code=&redirect_uri=&client_id=&client_secret=' + ``` + + - Windows + ```bash +curl --request POST ^ +--url https://api.hubapi.com/oauth/v1/token ^ +--header 'content-type: application/x-www-form-urlencoded' ^ +--data 'grant_type=authorization_code&code=&redirect_uri=&client_id=&client_secret=' + ``` + +This command will return the access token necessary for API calls. + +```json +{ + "token_type": "bearer", + "refresh_token": "", + "access_token": "", + "expires_in": 1800 +} +``` + + 5. Store the access token securely for use in your application. + ## Quickstart -[//]: # (TODO: Add a quickstart guide to demonstrate a basic functionality of the module, including sample code snippets.) +To use the HubSpot Marketing Campaigns connector in your Ballerina application, update the `.bal` file as follows: + +### Step 1: Import the module + +Import the `ballerinax/hubspot.marketing.campaigns` module and `ballerina/oauth2` module. + +```ballerina +import ballerinax/hubspot.marketing.campaigns as hsmcampaigns; +import ballerina/oauth2; +``` + +### Step 2: Instantiate a new connector + + 1. Create a `Config.toml` file and, configure the obtained credentials in the above steps as follows: + +```toml + clientId = + clientSecret = + refreshToken = +``` + + 2. Instantiate a `hsmcampaigns:ConnectionConfig` with the obtained credentials and initialize the connector with it. + +```ballerina +configurable string clientId = ?; +configurable string clientSecret = ?; +configurable string refreshToken = ?; + +final hsmcampaigns:ConnectionConfig hsmcampaignsConfig = { + auth : { + clientId, + clientSecret, + refreshToken, + credentialBearer: oauth2:POST_BODY_BEARER + } +}; + +final hsmcampaigns:Client hsmcampaignsClient = check new (hsmcampaignsConfig); +``` + +### Step 3: Invoke the connector operation + +Now, utilize the available connector operations. A sample usecase is shown below. + +Retrieve a Marketing Campaign + +```ballerina +public function main() returns error? { + hsmcampaigns:CollectionResponseWithTotalPublicCampaignForwardPaging campaigns = check hsmcampaignsClient->/marketing/v3/campaigns.get(); +} +``` + ## Examples -The `HubSpot Marketing Campaigns ` connector provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/module-ballerinax-hubspot.marketing.campaigns/tree/main/examples/), covering the following use cases: +The `HubSpot Marketing Campaigns ` connector provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/tree/main/examples), covering the following use cases: -[//]: # (TODO: Add examples) +1. [Batch of Campaigns](examples/batch_of_campaigns) - Fully manage a batch of campaigns +2. [Campaign Lifecycle with Assets](examples/campaign_lifecycle_with_assets) - Full life cycle of a campaign associated with assets such as forms ## Build from the source diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index c90dadb..5a6977c 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -5,12 +5,12 @@ name = "hubspot.marketing.campaigns" version = "1.0.0" license = ["Apache-2.0"] authors = ["Ballerina"] -keywords = [] -# icon = "icon.png" # TODO: update icon.png +keywords = ["hubspot", "crm", "marketing", "campaigns"] + repository = "https://github.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns" [build-options] observabilityIncluded = true -[platform.java21] +[platform.java17] graalvmCompatible = true diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml new file mode 100644 index 0000000..b9d58fe --- /dev/null +++ b/ballerina/Dependencies.toml @@ -0,0 +1,336 @@ +# AUTO-GENERATED FILE. DO NOT MODIFY. + +# This file is auto-generated by Ballerina for managing dependency versions. +# It should not be modified by hand. + +[ballerina] +dependencies-toml-version = "2" +distribution-version = "2201.10.3" + +[[package]] +org = "ballerina" +name = "auth" +version = "2.12.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "cache" +version = "3.8.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "constraint" +version = "1.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.7.2" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "file" +version = "1.10.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "http" +version = "2.12.4" +dependencies = [ + {org = "ballerina", name = "auth"}, + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "file"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.decimal"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "observe"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] +modules = [ + {org = "ballerina", packageName = "http", moduleName = "http"}, + {org = "ballerina", packageName = "http", moduleName = "http.httpscerr"} +] + +[[package]] +org = "ballerina" +name = "io" +version = "1.6.3" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"} +] + +[[package]] +org = "ballerina" +name = "jballerina.java" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "jwt" +version = "2.13.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] + +[[package]] +org = "ballerina" +name = "lang.decimal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.error" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.int" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "lang.regexp" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.runtime" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.string" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.regexp"} +] + +[[package]] +org = "ballerina" +name = "lang.value" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "log" +version = "2.10.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerina", packageName = "log", moduleName = "log"} +] + +[[package]] +org = "ballerina" +name = "mime" +version = "2.10.1" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "oauth2" +version = "2.12.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] +modules = [ + {org = "ballerina", packageName = "oauth2", moduleName = "oauth2"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.3.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "os" +version = "1.8.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "task" +version = "2.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "test" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.error"} +] +modules = [ + {org = "ballerina", packageName = "test", moduleName = "test"} +] + +[[package]] +org = "ballerina" +name = "time" +version = "2.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "time", moduleName = "time"} +] + +[[package]] +org = "ballerina" +name = "url" +version = "2.4.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "url", moduleName = "url"} +] + +[[package]] +org = "ballerinai" +name = "observe" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerinai", packageName = "observe", moduleName = "observe"} +] + +[[package]] +org = "ballerinax" +name = "hubspot.marketing.campaigns" +version = "1.0.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "test"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"}, + {org = "ballerinai", name = "observe"} +] +modules = [ + {org = "ballerinax", packageName = "hubspot.marketing.campaigns", moduleName = "hubspot.marketing.campaigns"} +] + diff --git a/ballerina/Module.md b/ballerina/Module.md index f793692..3c9aea4 100644 --- a/ballerina/Module.md +++ b/ballerina/Module.md @@ -1,17 +1,180 @@ ## Overview -[//]: # (TODO: Add overview mentioning the purpose of the module, supported REST API versions, and other high-level details.) +[HubSpot](https://www.hubspot.com) is an AI-powered customer relationship management (CRM) platform. + +The `ballerinax/module-ballerinax-hubspot.marketing.campaigns` connector offers APIs to connect and interact with the [Hubspot Marketing Campaigns API](https://developers.hubspot.com/docs/guides/api/marketing/campaigns) endpoints, specifically based on the [HubSpot REST API](https://developers.hubspot.com/docs/reference/api/overview) ## Setup guide -[//]: # (TODO: Add detailed steps to obtain credentials and configure the module.) +To use the HubSpot Marketing Campaigns connector, you must have access to the HubSpot API through a HubSpot developer account and a HubSpot App under it. Therefore you need to register for a developer account at HubSpot if you don't have one already. + +### Step 1: Create/Login to a HubSpot Developer Account + +If you have an account already, go to the [HubSpot developer portal](https://developers.hubspot.com/) + +If you don't have a HubSpot Developer Account you can sign up to a free account [here](https://developers.hubspot.com/get-started) + +### Step 2 (Optional): Create a Developer Test Account + +Within app developer accounts, you can create a [developer test account](https://developers.hubspot.com/docs/getting-started/account-types#developer-test-accounts) under your account to test apps and integrations without affecting any real HubSpot data. + +> **Note**: These accounts are only for development and testing purposes. In production you should not use Developer Test Accounts. + + 1. Go to Test accounts section from the left sidebar. + +![Test Account](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/testAccount.png) + + 2. Click on the Create developer test account button on the top right corner. + +![Development Test Account](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/developmentTestAccount.png) + + 3. In the pop-up window, provide a name for the test account and click on the Create button. + +![Create Account](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/createAccount.png) + + 4. You will see the newly created test account in the list of test accounts. + +![Test Account Portal](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/testAccountPortal.png) + +### Step 3: Create a HubSpot App + + 1. Navigate to the Apps section from the left sidebar and click on the Create app button on the top right corner. + +![App Section](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/appSection.png) + + 2. Provide a public app name and description for your app. + +![Naming the App](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/namingApp.png) + +### Step 4: Setup Authentication + + 1. Move to the Auth tab. + +![Auth Section](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/auth.png) + + 2. In the Scopes section, add the following scopes for your app using the Add new scopes button. + - `marketing.campaigns.read` + - `marketing.campaigns.revenue.read` + - `marketing.campaigns.write` + +![Marketing Scopes](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/marketingScopes.png) + + 3. In the Redirect URL section, add the redirect URL for your app. This is the URL where the user will be redirected after the authentication process. You can use `localhost` for testing purposes. Then click the "Create App" button. + +![Redirect URL](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/redirectURL.png) + +### Step 5: Get the Client ID and Client Secret + +Navigate to the Auth tab and you will see the `Client ID`, and `Client Secret` for your app. Make sure to save these values. + +![Client ID and Client Secret](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/clientId_secretId.png) + +### Step 6: Setup Authentication Flow + +Before proceeding with the Quickstart, ensure you have obtained the Access Token using the following steps: + + 1. Create an authorization URL using the following format: + + ``` + https://app.hubspot.com/oauth/authorize?client_id=&scope=&redirect_uri= + ``` + Replace the ``, ``, and `` with your specific value. + + 2. Paste it in the browser and select your developer test account to intall the app when prompted. + +![Account Select](https://github.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/blob/fb6603714f9b563b74775579577749ad62726576/docs/resources/accountSelect.png) + + 3. A code will be displayed in the browser. Copy the code. + + 4. Run the following curl command. Replace the ``, ``, and `` with your specific value. Use the code you received in the above step 3 as the CODE below + + - Linux/macOS + ```bash + curl --request POST \ +--url https://api.hubapi.com/oauth/v1/token \ +--header 'content-type: application/x-www-form-urlencoded' \ +--data 'grant_type=authorization_code&code=&redirect_uri=&client_id=&client_secret=' + ``` + - Windows + ```bash + curl --request POST ^ +--url https://api.hubapi.com/oauth/v1/token ^ +--header 'content-type: application/x-www-form-urlencoded' ^ +--data 'grant_type=authorization_code&code=&redirect_uri=&client_id=&client_secret=' + ``` + +This command will return the access token necessary for API calls. + +```json +{ + "token_type": "bearer", + "refresh_token": "", + "access_token": "", + "expires_in": 1800 +} +``` + + 5. Store the access token securely for use in your application. + ## Quickstart -[//]: # (TODO: Add a quickstart guide to demonstrate a basic functionality of the module, including sample code snippets.) +To use the HubSpot Marketing Campaigns connector in your Ballerina application, update the `.bal` file as follows: + +### Step 1: Import the module + +Import the `ballerinax/hubspot.marketing.campaigns` module and `ballerina/oauth2` module. + +```ballerina +import ballerinax/hubspot.marketing.campaigns as hsmcampaigns; +import ballerina/oauth2; +``` + +### Step 2: Instantiate a new connector + + 1. Create a `Config.toml` file and, configure the obtained credentials in the above steps as follows: + +```toml + clientId = + clientSecret = + refreshToken = +``` + + 2. Instantiate a `hsmcampaigns:ConnectionConfig` with the obtained credentials and initialize the connector with it. + +```ballerina +configurable string clientId = ?; +configurable string clientSecret = ?; +configurable string refreshToken = ?; + +final hsmcampaigns:ConnectionConfig hsmcampaignsConfig = { + auth : { + clientId, + clientSecret, + refreshToken, + credentialBearer: oauth2:POST_BODY_BEARER + } +}; + +final hsmcampaigns:Client hsmcampaignsClient = check new (hsmcampaignsConfig); +``` + +### Step 3: Invoke the connector operation + +Now, utilize the available connector operations. A sample usecase is shown below. + +Retrieve a Marketing Campaign + +```ballerina +public function main() returns error? { + hsmcampaigns:CollectionResponseWithTotalPublicCampaignForwardPaging campaigns = check hsmcampaignsClient->/marketing/v3/campaigns.get(); +} +``` + ## Examples -The `HubSpot Marketing Campaigns ` connector provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/module-ballerinax-hubspot.marketing.campaigns/tree/main/examples/), covering the following use cases: +The `HubSpot Marketing Campaigns` connector provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/tree/main/examples), covering the following use cases: -[//]: # (TODO: Add examples) +1. [Batch of Campaigns](examples/batch_of_campaigns) - Fully manage a batch of campaigns +2. [Campaign Lifecycle with Assets](examples/campaign_lifecycle_with_assets) - Full life cycle of a campaign associated with assets such as forms diff --git a/ballerina/Package.md b/ballerina/Package.md index f793692..2476e19 100644 --- a/ballerina/Package.md +++ b/ballerina/Package.md @@ -1,17 +1,181 @@ ## Overview -[//]: # (TODO: Add overview mentioning the purpose of the module, supported REST API versions, and other high-level details.) +[HubSpot](https://www.hubspot.com) is an AI-powered customer relationship management (CRM) platform. + +The `ballerinax/module-ballerinax-hubspot.marketing.campaigns` connector offers APIs to connect and interact with the [Hubspot Marketing Campaigns API](https://developers.hubspot.com/docs/guides/api/marketing/campaigns) endpoints, specifically based on the [HubSpot REST API](https://developers.hubspot.com/docs/reference/api/overview) ## Setup guide -[//]: # (TODO: Add detailed steps to obtain credentials and configure the module.) +To use the HubSpot Marketing Campaigns connector, you must have access to the HubSpot API through a HubSpot developer account and a HubSpot App under it. Therefore you need to register for a developer account at HubSpot if you don't have one already. + +### Step 1: Create/Login to a HubSpot Developer Account + +If you have an account already, go to the [HubSpot developer portal](https://developers.hubspot.com/) + +If you don't have a HubSpot Developer Account you can sign up to a free account [here](https://developers.hubspot.com/get-started) + +### Step 2 (Optional): Create a Developer Test Account + +Within app developer accounts, you can create a [developer test account](https://developers.hubspot.com/docs/getting-started/account-types#developer-test-accounts) under your account to test apps and integrations without affecting any real HubSpot data. + +> **Note**: These accounts are only for development and testing purposes. In production you should not use Developer Test Accounts. + + 1. Go to Test accounts section from the left sidebar. + +![Test Account](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/testAccount.png) + + 2. Click on the Create developer test account button on the top right corner. + +![Development Test Account](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/developmentTestAccount.png) + + 3. In the pop-up window, provide a name for the test account and click on the Create button. + +![Create Account](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/createAccount.png) + + 4. You will see the newly created test account in the list of test accounts. + +![Test Account Portal](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/testAccountPortal.png) + +### Step 3: Create a HubSpot App + + 1. Navigate to the Apps section from the left sidebar and click on the Create app button on the top right corner. + +![App Section](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/appSection.png) + + 2. Provide a public app name and description for your app. + +![Naming the App](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/namingApp.png) + +### Step 4: Setup Authentication + + 1. Move to the Auth tab. + +![Auth Section](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/auth.png) + + 2. In the Scopes section, add the following scopes for your app using the Add new scopes button. + - `marketing.campaigns.read` + - `marketing.campaigns.revenue.read` + - `marketing.campaigns.write` + +![Marketing Scopes](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/marketingScopes.png) + + 3. In the Redirect URL section, add the redirect URL for your app. This is the URL where the user will be redirected after the authentication process. You can use `localhost` for testing purposes. Then click the "Create App" button. + +![Redirect URL](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/redirectURL.png) + +### Step 5: Get the Client ID and Client Secret + +Navigate to the Auth tab and you will see the `Client ID`, and `Client Secret` for your app. Make sure to save these values. + +![Client ID and Client Secret](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/clientId_secretId.png) + +### Step 6: Setup Authentication Flow + +Before proceeding with the Quickstart, ensure you have obtained the Access Token using the following steps: + + 1. Create an authorization URL using the following format: + + ``` + https://app.hubspot.com/oauth/authorize?client_id=&scope=&redirect_uri= + ``` + Replace the ``, `` and `` with your specific value. + + 2. Paste it in the browser and select your developer test account to intall the app when prompted. + +![Account Select](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/main/docs/resources/accountSelect.png) + + 3. A code will be displayed in the browser. Copy the code. + + 4. Run the following curl command. Replace the ``, ``, and `` with your specific value. Use the code you received in the above step 3 as the CODE below + + - Linux/macOS + ```bash + curl --request POST \ +--url https://api.hubapi.com/oauth/v1/token \ +--header 'content-type: application/x-www-form-urlencoded' \ +--data 'grant_type=authorization_code&code=&redirect_uri=&client_id=&client_secret=' + ``` + + - Windows + ```bash +curl --request POST ^ +--url https://api.hubapi.com/oauth/v1/token ^ +--header 'content-type: application/x-www-form-urlencoded' ^ +--data 'grant_type=authorization_code&code=&redirect_uri=&client_id=&client_secret=' + ``` + +This command will return the access token necessary for API calls. + +```json +{ + "token_type": "bearer", + "refresh_token": "", + "access_token": "", + "expires_in": 1800 +} +``` + + 5. Store the access token securely for use in your application. + ## Quickstart -[//]: # (TODO: Add a quickstart guide to demonstrate a basic functionality of the module, including sample code snippets.) +To use the HubSpot Marketing Campaigns connector in your Ballerina application, update the `.bal` file as follows: + +### Step 1: Import the module + +Import the `ballerinax/hubspot.marketing.campaigns` module and `ballerina/oauth2` module. + +```ballerina +import ballerinax/hubspot.marketing.campaigns as hsmcampaigns; +import ballerina/oauth2; +``` + +### Step 2: Instantiate a new connector + + 1. Create a `Config.toml` file and, configure the obtained credentials in the above steps as follows: + +```toml + clientId = + clientSecret = + refreshToken = +``` + + 2. Instantiate a `hsmcampaigns:ConnectionConfig` with the obtained credentials and initialize the connector with it. + +```ballerina +configurable string clientId = ?; +configurable string clientSecret = ?; +configurable string refreshToken = ?; + +final hsmcampaigns:ConnectionConfig hsmcampaignsConfig = { + auth : { + clientId, + clientSecret, + refreshToken, + credentialBearer: oauth2:POST_BODY_BEARER + } +}; + +final hsmcampaigns:Client hsmcampaignsClient = check new (hsmcampaignsConfig); +``` + +### Step 3: Invoke the connector operation + +Now, utilize the available connector operations. A sample usecase is shown below. + +Retrieve a Marketing Campaign + +```ballerina +public function main() returns error? { + hsmcampaigns:CollectionResponseWithTotalPublicCampaignForwardPaging campaigns = check hsmcampaignsClient->/marketing/v3/campaigns.get(); +} +``` + ## Examples -The `HubSpot Marketing Campaigns ` connector provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/module-ballerinax-hubspot.marketing.campaigns/tree/main/examples/), covering the following use cases: +The `HubSpot Marketing Campaigns ` connector provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns/tree/main/examples), covering the following use cases: -[//]: # (TODO: Add examples) +1. [Batch of Campaigns](examples/batch_of_campaigns) - Fully manage a batch of campaigns +2. [Campaign Lifecycle with Assets](examples/campaign_lifecycle_with_assets) - Full life cycle of a campaign associated with assets such as forms diff --git a/ballerina/client.bal b/ballerina/client.bal index 66cdc3f..7086fa6 100644 --- a/ballerina/client.bal +++ b/ballerina/client.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). // // WSO2 LLC. licenses this file to you under the Apache License, // Version 2.0 (the "License"); you may not use this file except @@ -13,3 +13,326 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. + +import ballerina/http; + +public isolated client class Client { + final http:Client clientEp; + final readonly & ApiKeysConfig? apiKeyConfig; + # Gets invoked to initialize the `connector`. + # + # + config - The configurations to be used when initializing the `connector` + # + serviceUrl - URL of the target service + # + return - An error if connector initialization failed + public isolated function init(ConnectionConfig config, string serviceUrl = "https://api.hubapi.com/marketing/v3/campaigns") returns error? { + http:ClientConfiguration httpClientConfig = {httpVersion: config.httpVersion, timeout: config.timeout, forwarded: config.forwarded, poolConfig: config.poolConfig, compression: config.compression, circuitBreaker: config.circuitBreaker, retryConfig: config.retryConfig, validation: config.validation}; + do { + if config.http1Settings is ClientHttp1Settings { + ClientHttp1Settings settings = check config.http1Settings.ensureType(ClientHttp1Settings); + httpClientConfig.http1Settings = {...settings}; + } + if config.http2Settings is http:ClientHttp2Settings { + httpClientConfig.http2Settings = check config.http2Settings.ensureType(http:ClientHttp2Settings); + } + if config.cache is http:CacheConfig { + httpClientConfig.cache = check config.cache.ensureType(http:CacheConfig); + } + if config.responseLimits is http:ResponseLimitConfigs { + httpClientConfig.responseLimits = check config.responseLimits.ensureType(http:ResponseLimitConfigs); + } + if config.secureSocket is http:ClientSecureSocket { + httpClientConfig.secureSocket = check config.secureSocket.ensureType(http:ClientSecureSocket); + } + if config.proxy is http:ProxyConfig { + httpClientConfig.proxy = check config.proxy.ensureType(http:ProxyConfig); + } + } + if config.auth is ApiKeysConfig { + self.apiKeyConfig = (config.auth).cloneReadOnly(); + } else { + httpClientConfig.auth = config.auth; + self.apiKeyConfig = (); + } + http:Client httpEp = check new (serviceUrl, httpClientConfig); + self.clientEp = httpEp; + return; + } + + # Delete campaign + # + # + campaignGuid - Unique identifier for the campaign, formatted as a UUID. + # + headers - Headers to be sent with the request + # + return - No content + resource isolated function delete [string campaignGuid](map headers = {}) returns http:Response|error { + string resourcePath = string `/${getEncodedUri(campaignGuid)}`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + return self.clientEp->delete(resourcePath, headers = httpHeaders); + } + + # Remove asset association + # + # + campaignGuid - Unique identifier for the campaign, formatted as a UUID. + # + assetType - The type of asset + # Important: Currently, only the following asset types are available for disassociation via the API: FORM, OBJECT_LIST, EXTERNAL_WEB_URL + # + assetId - Id of the asset + # + headers - Headers to be sent with the request + # + return - No content + resource isolated function delete [string campaignGuid]/assets/[string assetType]/[string assetId](map headers = {}) returns http:Response|error { + string resourcePath = string `/${getEncodedUri(campaignGuid)}/assets/${getEncodedUri(assetType)}/${getEncodedUri(assetId)}`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + return self.clientEp->delete(resourcePath, headers = httpHeaders); + } + + # Campaign search + # + # + headers - Headers to be sent with the request + # + queries - Queries to be sent with the request + # + return - successful operation + resource isolated function get .(map headers = {}, *GetMarketingV3CampaignsQueries queries) returns CollectionResponseWithTotalPublicCampaignForwardPaging|error { + string resourcePath = string `/`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map queryParamEncoding = {"properties": {style: FORM, explode: true}}; + resourcePath = resourcePath + check getPathForQueryParam(queries, queryParamEncoding); + map httpHeaders = getMapForHeaders(headerValues); + return self.clientEp->get(resourcePath, httpHeaders); + } + + # Read a campaign + # + # + campaignGuid - Unique identifier for the campaign, formatted as a UUID. + # + headers - Headers to be sent with the request + # + queries - Queries to be sent with the request + # + return - successful operation + resource isolated function get [string campaignGuid](map headers = {}, *GetMarketingV3CampaignsCampaignguidQueries queries) returns PublicCampaignWithAssets|error { + string resourcePath = string `/${getEncodedUri(campaignGuid)}`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map queryParamEncoding = {"properties": {style: FORM, explode: true}}; + resourcePath = resourcePath + check getPathForQueryParam(queries, queryParamEncoding); + map httpHeaders = getMapForHeaders(headerValues); + return self.clientEp->get(resourcePath, httpHeaders); + } + + # List assets + # + # + campaignGuid - Unique identifier for the campaign, formatted as a UUID. + # + assetType - The type of asset to fetch. + # + headers - Headers to be sent with the request + # + queries - Queries to be sent with the request + # + return - successful operation + resource isolated function get [string campaignGuid]/assets/[string assetType](map headers = {}, *GetMarketingV3CampaignsCampaignguidAssetsAssettypeQueries queries) returns CollectionResponsePublicCampaignAssetForwardPaging|error { + string resourcePath = string `/${getEncodedUri(campaignGuid)}/assets/${getEncodedUri(assetType)}`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + resourcePath = resourcePath + check getPathForQueryParam(queries); + map httpHeaders = getMapForHeaders(headerValues); + return self.clientEp->get(resourcePath, httpHeaders); + } + + # Read budget + # + # + campaignGuid - Unique identifier for the campaign, formatted as a UUID. + # + headers - Headers to be sent with the request + # + return - successful operation + resource isolated function get [string campaignGuid]/budget/totals(map headers = {}) returns PublicBudgetTotals|error { + string resourcePath = string `/${getEncodedUri(campaignGuid)}/budget/totals`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + return self.clientEp->get(resourcePath, httpHeaders); + } + + # Fetch contact IDs + # + # + campaignGuid - Unique identifier for the campaign, formatted as a UUID. + # + contactType - The type of metric to filter the influenced contacts. Allowed values: contactFirstTouch, contactLastTouch, influencedContacts + # + headers - Headers to be sent with the request + # + queries - Queries to be sent with the request + # + return - successful operation + resource isolated function get [string campaignGuid]/reports/contacts/[string contactType](map headers = {}, *GetMarketingV3CampaignsCampaignguidReportsContactsContacttypeQueries queries) returns CollectionResponseContactReferenceForwardPaging|error { + string resourcePath = string `/${getEncodedUri(campaignGuid)}/reports/contacts/${getEncodedUri(contactType)}`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + resourcePath = resourcePath + check getPathForQueryParam(queries); + map httpHeaders = getMapForHeaders(headerValues); + return self.clientEp->get(resourcePath, httpHeaders); + } + + # Get Campaign Metrics + # + # + campaignGuid - Unique identifier for the campaign, formatted as a UUID. + # + headers - Headers to be sent with the request + # + queries - Queries to be sent with the request + # + return - successful operation + resource isolated function get [string campaignGuid]/reports/metrics(map headers = {}, *GetMarketingV3CampaignsCampaignguidReportsMetricsQueries queries) returns MetricsCounters|error { + string resourcePath = string `/${getEncodedUri(campaignGuid)}/reports/metrics`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + resourcePath = resourcePath + check getPathForQueryParam(queries); + map httpHeaders = getMapForHeaders(headerValues); + return self.clientEp->get(resourcePath, httpHeaders); + } + + # Fetch revenue + # + # + campaignGuid - Unique identifier for the campaign, formatted as a UUID. + # + headers - Headers to be sent with the request + # + queries - Queries to be sent with the request + # + return - successful operation + resource isolated function get [string campaignGuid]/reports/revenue(map headers = {}, *GetMarketingV3CampaignsCampaignguidReportsRevenueQueries queries) returns RevenueAttributionAggregate|error { + string resourcePath = string `/${getEncodedUri(campaignGuid)}/reports/revenue`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + resourcePath = resourcePath + check getPathForQueryParam(queries); + map httpHeaders = getMapForHeaders(headerValues); + return self.clientEp->get(resourcePath, httpHeaders); + } + + # Update campaign + # + # + campaignGuid - Unique identifier for the campaign, formatted as a UUID. + # + headers - Headers to be sent with the request + # + return - successful operation + resource isolated function patch [string campaignGuid](PublicCampaignInput payload, map headers = {}) returns PublicCampaign|error { + string resourcePath = string `/${getEncodedUri(campaignGuid)}`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->patch(resourcePath, request, httpHeaders); + } + + # Create a campaign + # + # + headers - Headers to be sent with the request + # + return - successful operation + resource isolated function post .(PublicCampaignInput payload, map headers = {}) returns PublicCampaign|error { + string resourcePath = string `/`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->post(resourcePath, request, httpHeaders); + } + + # Delete a batch of campaigns + # + # + headers - Headers to be sent with the request + # + return - No content + resource isolated function post batch/archive(BatchInputPublicCampaignDeleteInput payload, map headers = {}) returns http:Response|error { + string resourcePath = string `/batch/archive`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->post(resourcePath, request, httpHeaders); + } + + # Create a batch of campaigns + # + # + headers - Headers to be sent with the request + # + return - successful operation + resource isolated function post batch/create(BatchInputPublicCampaignInput payload, map headers = {}) returns BatchResponsePublicCampaign|BatchResponsePublicCampaignWithErrors|error { + string resourcePath = string `/batch/create`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->post(resourcePath, request, httpHeaders); + } + + # Read a batch of campaigns + # + # + headers - Headers to be sent with the request + # + queries - Queries to be sent with the request + # + return - successful operation + resource isolated function post batch/read(BatchInputPublicCampaignReadInput payload, map headers = {}, *PostMarketingV3CampaignsBatchReadQueries queries) returns BatchResponsePublicCampaignWithAssets|BatchResponsePublicCampaignWithAssetsWithErrors|error { + string resourcePath = string `/batch/read`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map queryParamEncoding = {"properties": {style: FORM, explode: true}}; + resourcePath = resourcePath + check getPathForQueryParam(queries, queryParamEncoding); + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->post(resourcePath, request, httpHeaders); + } + + # Update a batch of campaigns + # + # + headers - Headers to be sent with the request + # + return - successful operation + resource isolated function post batch/update(BatchInputPublicCampaignBatchUpdateItem payload, map headers = {}) returns BatchResponsePublicCampaign|BatchResponsePublicCampaignWithErrors|error { + string resourcePath = string `/batch/update`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->post(resourcePath, request, httpHeaders); + } + + # Add asset association + # + # + campaignGuid - Unique identifier for the campaign, formatted as a UUID + # + assetType - The type of asset + # Important: Currently, only the following asset types are available for association via the API: FORM, OBJECT_LIST, EXTERNAL_WEB_URL + # + assetId - Id of the asset + # + headers - Headers to be sent with the request + # + return - No content + resource isolated function put [string campaignGuid]/assets/[string assetType]/[string assetId](map headers = {}) returns http:Response|error { + string resourcePath = string `/${getEncodedUri(campaignGuid)}/assets/${getEncodedUri(assetType)}/${getEncodedUri(assetId)}`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + return self.clientEp->put(resourcePath, request, httpHeaders); + } +} diff --git a/ballerina/tests/mock_service.bal b/ballerina/tests/mock_service.bal new file mode 100644 index 0000000..17aa41c --- /dev/null +++ b/ballerina/tests/mock_service.bal @@ -0,0 +1,55 @@ +import ballerina/http; +import ballerina/log; + +listener http:Listener httpListener = new (9090); + +http:Service mockService = service object { + resource function post marketing/v3/campaigns() returns PublicCampaign{ + return { + id: "12345_Mock", + properties: { + "hs_name": "campaignMock", + "hs_goal": "campaignGoalSpecifiedMock", + "hs_notes": "someNotesForTheCampaignMock" + }, + createdAt:"2021-08-25T10:00:00Z", + updatedAt: "2021-08-25T10:00:00Z" + }; + }; + + resource function get marketing/v3/campaigns/[string campaignMockGuid]/assets/FORM() returns CollectionResponsePublicCampaignAssetForwardPaging { + return { + "results": [ + { + "id": "88047023-7777-40a1-b74b-4b1139e8d45b", + "name": "New form (January 3, 2025 4:37:41 AM EST)" + } + ] + }; + }; + + resource function get marketing/v3/campaigns/[string campaignMockGuid]() returns PublicCampaignWithAssets { + return { + "id": "c4573779-0830-4eb3-bfa3-0916bda9c1a4", + "properties": {}, + "createdAt": "2025-01-07T04:45:53.011Z", + "updatedAt": "2025-01-08T09:52:24.054Z", + "assets": { + "FORM": { + "results": [ + { + "id": "88047023-7777-40a1-b74b-4b1139e8d45b", + "name": "New form (January 3, 2025 4:37:41 AM EST)" + } + ] + } + } + }; + }; +}; + +function init() returns error? { + log:printInfo("Initializing mock service"); + check httpListener.attach(mockService, "/"); + check httpListener.'start(); +} diff --git a/ballerina/tests/mock_tests.bal b/ballerina/tests/mock_tests.bal new file mode 100644 index 0000000..3647b66 --- /dev/null +++ b/ballerina/tests/mock_tests.bal @@ -0,0 +1,54 @@ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/test; + +final Client mockClient = check new (config, serviceUrl = "http://localhost:8080/marketing/v3/campaigns"); + +final string campaignMockGuid = "c4573779-0830-4eb3-bfa3-0916bda9c1a4"; +final string assetMockType = "FORM"; + +@test:Config { + groups: ["mock_tests"] +} +isolated function mockTestCreateCampaign() returns error? { + PublicCampaign response = check mockClient->/.post( + payload = { + properties: { + "hs_name": "campaignMock", + "hs_goal": "campaignGoalSpecifiedMock", + "hs_notes": "someNotesForTheCampaignMock" + } + } + ); + test:assertNotEquals(response?.id, ""); +} + +@test:Config { + groups: ["mock_tests"] +} +isolated function testMockGetReadCampaign() returns error? { + PublicCampaignWithAssets response = check baseClient->/[campaignMockGuid]; + test:assertEquals(response?.id, campaignGuid); +} + +@test:Config { + groups: ["mock_tests"] +} +isolated function testMockGetListAssets() returns error? { + CollectionResponsePublicCampaignAssetForwardPaging response = check baseClient->/[campaignMockGuid]/assets/[assetMockType]; + test:assertTrue(response?.results.length() > 0); +} diff --git a/ballerina/tests/test.bal b/ballerina/tests/test.bal new file mode 100644 index 0000000..4130ac4 --- /dev/null +++ b/ballerina/tests/test.bal @@ -0,0 +1,219 @@ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/oauth2; +import ballerina/test; +import ballerina/time; + +configurable string clientId = ?; +configurable string clientSecret = ?; +configurable string refreshToken = ?; + +OAuth2RefreshTokenGrantConfig auth = { + clientId, + clientSecret, + refreshToken, + credentialBearer: oauth2:POST_BODY_BEARER +}; + +ConnectionConfig config = {auth}; + +final Client baseClient = check new (config); + +string campaignGuid2 = ""; +configurable string campaignGuid = ?; +configurable string assetType = ?; +configurable string assetID = ?; + +configurable string sampleCampaignGuid1 = ?; +configurable string sampleCampaignGuid2 = ?; +configurable string sampleCampaignGuid3 = ?; +configurable string sampleCampaignGuid4 = ?; + +@test:Config { + groups: ["live_tests"] +} +isolated function testGetSearchMarketingCampaigns() returns error? { + CollectionResponseWithTotalPublicCampaignForwardPaging response = check baseClient->/.get(); + test:assertTrue(response?.results.length() > 0); +} + +@test:Config { + groups: ["live_tests"] +} +function testPostCreateMarketingCampaigns() returns error? { + PublicCampaign response = check baseClient->/.post( + payload = { + properties: { + "hs_name": "campaign" + time:utcNow().toString(), + "hs_goal": "campaignGoalSpecified", + "hs_notes": "someNotesForTheCampaign" + } + } + ); + test:assertNotEquals(response?.id, ""); + campaignGuid2 = response?.id; +} + +@test:Config { + groups: ["live_tests"] +} +isolated function testGetReadACampaign() returns error? { + PublicCampaignWithAssets response = check baseClient->/[campaignGuid]; + test:assertEquals(response?.id, campaignGuid); +} + +@test:Config { + groups: ["live_tests"] +} +isolated function testPatchUpdateCampaigns() returns error? { + PublicCampaign response = check baseClient->/[campaignGuid].patch( + payload = { + properties: { + "hs_goal": "updatedCampaignGoal", + "hs_notes": "updatedNotesForTheCampaign" + } + } + ); + test:assertEquals(response?.id, campaignGuid); +} + +@test:Config { + groups: ["live_tests"] +} +isolated function testPostBatchCreate() returns error? { + BatchResponsePublicCampaign|BatchResponsePublicCampaignWithErrors response = check baseClient->/batch/create.post( + payload = { + "inputs": [ + { + "properties": { + "hs_name": "batchCampaign" + time:utcToString(time:utcNow()), + "hs_goal": "batchCampaignGoalSpecified" + } + } + ] + } + ); + test:assertEquals(response?.status, "COMPLETE"); +} + +@test:Config { + groups: ["live_tests"] +} +isolated function testPostBatchUpdate() returns error? { + BatchResponsePublicCampaign|BatchResponsePublicCampaignWithErrors response = check baseClient->/batch/update.post( + payload = { + "inputs": [ + { + "id": sampleCampaignGuid1, + "properties": { + "hs_goal": "updatedGoal", + "hs_notes": "updatedNote" + } + } + ] + } + ); + test:assertEquals(response?.status, "COMPLETE"); +} + +@test:Config { + groups: ["live_tests"] +} +isolated function testPostBatchRead() returns error? { + BatchResponsePublicCampaignWithAssets|BatchResponsePublicCampaignWithAssetsWithErrors response = + check baseClient->/batch/read.post( + payload = { + "inputs": [ + { + "id": sampleCampaignGuid2 + } + ] + } + ); + test:assertEquals(response?.status, "COMPLETE"); +} + +@test:Config { + groups: ["live_tests"] +} +isolated function testGetReportsRevenue() returns error? { + RevenueAttributionAggregate response = check baseClient->/[campaignGuid]/reports/revenue; + test:assertTrue(response?.revenueAmount is decimal); +} + +@test:Config { + groups: ["live_tests"] +} +isolated function testGetReportsMetrics() returns error? { + MetricsCounters response = check baseClient->/[campaignGuid]/reports/metrics; + test:assertTrue(response?.sessions >= 0); +} + +@test:Config { + groups: ["live_tests"] +} +isolated function testGetListAssets() returns error? { + CollectionResponsePublicCampaignAssetForwardPaging response = check baseClient->/[campaignGuid]/assets/[assetType]; + test:assertTrue(response?.results.length() > 0); + +} + +@test:Config { + dependsOn: [testDeleteRemoveAssetAssociation], + groups: ["live_tests"] +} +isolated function testPutAddAssetAssociation() returns error? { + var response = check baseClient->/[campaignGuid]/assets/[assetType]/[assetID].put(); + test:assertEquals(response.statusCode, 204); +} + +@test:Config { + dependsOn: [testGetListAssets], + groups: ["live_tests"] +} +isolated function testDeleteRemoveAssetAssociation() returns error? { + var response = check baseClient->/[campaignGuid]/assets/[assetType]/[assetID].delete(); + test:assertEquals(response.statusCode, 204); +} + +@test:Config { + dependsOn: [testPostCreateMarketingCampaigns], + groups: ["live_tests"] +} +function testDeleteCampaign() returns error? { + var response = check baseClient->/[campaignGuid2].delete(); + test:assertEquals(response.statusCode, 204); +} + +@test:Config { + groups: ["live_tests"] +} +isolated function testPostDeleteABatchOfCampaigns() returns error? { + var response = check baseClient->/batch/archive.post( + payload = { + "inputs": [ + { + "id": sampleCampaignGuid3 + }, + { + "id": sampleCampaignGuid4 + } + ] + } + ); + test:assertEquals(response.statusCode, 204); +} diff --git a/ballerina/types.bal b/ballerina/types.bal new file mode 100644 index 0000000..b8f4a60 --- /dev/null +++ b/ballerina/types.bal @@ -0,0 +1,389 @@ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; + +public type StandardError record { + record {} subCategory?; + record {|string[]...;|} context; + record {|string...;|} links; + string id?; + string category; + string message; + ErrorDetail[] errors; + string status; +}; + +public type PublicCampaignBatchUpdateItem record { + string id; + record {|string...;|} properties; +}; + +public type CollectionResponsePublicCampaignAssetForwardPaging record { + ForwardPaging paging?; + PublicCampaignAsset[] results; +}; + +public type PublicCampaignReadInput record { + string id; +}; + +public type BatchResponsePublicCampaignWithAssetsWithErrors record { + string completedAt; + int:Signed32 numErrors?; + string requestedAt?; + string startedAt; + record {|string...;|} links?; + PublicCampaignWithAssets[] results; + StandardError[] errors?; + "PENDING"|"PROCESSING"|"CANCELED"|"COMPLETE" status; +}; + +public type BatchInputPublicCampaignReadInput record { + PublicCampaignReadInput[] inputs; +}; + +public type BatchResponsePublicCampaignWithAssets record { + string completedAt; + string requestedAt?; + string startedAt; + record {|string...;|} links?; + PublicCampaignWithAssets[] results; + "PENDING"|"PROCESSING"|"CANCELED"|"COMPLETE" status; +}; + +public type ErrorDetail record { + # A specific category that contains more specific detail about the error + string subCategory?; + # The status code associated with the error detail + string code?; + # The name of the field or parameter in which the error was found. + string 'in?; + # Context about the error condition + record {|string[]...;|} context?; + # A human readable message describing the error along with remediation steps where appropriate + string message; +}; + +public type BatchResponsePublicCampaignWithErrors record { + string completedAt; + int:Signed32 numErrors?; + string requestedAt?; + string startedAt; + record {|string...;|} links?; + PublicCampaign[] results; + StandardError[] errors?; + "PENDING"|"PROCESSING"|"CANCELED"|"COMPLETE" status; +}; + +public type ForwardPaging record { + NextPage next?; +}; + +# Represents the Queries record for the operation: get-/marketing/v3/campaigns/{campaignGuid}/reports/revenue +public type GetMarketingV3CampaignsCampaignguidReportsRevenueQueries record { + # Allowed values: LINEAR, FIRST_INTERACTION, LAST_INTERACTION, FULL_PATH, U_SHAPED, W_SHAPED, TIME_DECAY, J_SHAPED, INVERSE_J_SHAPED + # Default value: LINEAR + string attributionModel?; + # End date for the report data, formatted as YYYY-MM-DD. + # Default value: Current date + string endDate?; + # The start date for the report data, formatted as YYYY-MM-DD. + # Default value: 2006-01-01 + string startDate?; +}; + +# Represents the Queries record for the operation: post-/marketing/v3/campaigns/batch/read +public type PostMarketingV3CampaignsBatchReadQueries record { + # End date to fetch asset metrics, formatted as YYYY-MM-DD. This date is used to fetch the metrics associated with the assets for a specified period. If not provided, no asset metrics will be fetched. + string endDate?; + # Start date to fetch asset metrics, formatted as YYYY-MM-DD. This date is used to fetch the metrics associated with the assets for a specified period. If not provided, no asset metrics will be fetched. + string startDate?; + # A comma-separated list of the properties to be returned in the response. If any of the specified properties has empty value on the requested object(s), they will be ignored and not returned in response. If this parameter is empty, the response will include an empty properties map. + string[] properties?; +}; + +public type PublicSpendItem record { + int:Signed32 createdAt; + decimal amount; + string name; + string description?; + string id; + int:Signed32 'order; + int:Signed32 updatedAt; +}; + +public type BatchResponsePublicCampaign record { + string completedAt; + string requestedAt?; + string startedAt; + record {|string...;|} links?; + PublicCampaign[] results; + "PENDING"|"PROCESSING"|"CANCELED"|"COMPLETE" status; +}; + +# OAuth2 Refresh Token Grant Configs +public type OAuth2RefreshTokenGrantConfig record {| + *http:OAuth2RefreshTokenGrantConfig; + # Refresh URL + string refreshUrl = "https://api.hubapi.com/oauth/v1/token"; +|}; + +public type PublicCampaignWithAssets record { + string createdAt; + record {|CollectionResponsePublicCampaignAsset...;|} assets; + string id; + record {|string...;|} properties; + string updatedAt; +}; + +# Provides a set of configurations for controlling the behaviours when communicating with a remote HTTP endpoint. +@display {label: "Connection Config"} +public type ConnectionConfig record {| + # Provides Auth configurations needed when communicating with a remote HTTP endpoint. + http:BearerTokenConfig|OAuth2RefreshTokenGrantConfig|ApiKeysConfig auth; + # The HTTP version understood by the client + http:HttpVersion httpVersion = http:HTTP_2_0; + # Configurations related to HTTP/1.x protocol + ClientHttp1Settings http1Settings?; + # Configurations related to HTTP/2 protocol + http:ClientHttp2Settings http2Settings?; + # The maximum time to wait (in seconds) for a response before closing the connection + decimal timeout = 60; + # The choice of setting `forwarded`/`x-forwarded` header + string forwarded = "disable"; + # Configurations associated with request pooling + http:PoolConfiguration poolConfig?; + # HTTP caching related configurations + http:CacheConfig cache?; + # Specifies the way of handling compression (`accept-encoding`) header + http:Compression compression = http:COMPRESSION_AUTO; + # Configurations associated with the behaviour of the Circuit Breaker + http:CircuitBreakerConfig circuitBreaker?; + # Configurations associated with retrying + http:RetryConfig retryConfig?; + # Configurations associated with inbound response size limits + http:ResponseLimitConfigs responseLimits?; + # SSL/TLS-related options + http:ClientSecureSocket secureSocket?; + # Proxy server related options + http:ProxyConfig proxy?; + # Enables the inbound payload validation functionality which provided by the constraint package. Enabled by default + boolean validation = true; +|}; + +public type PublicBudgetTotals record { + PublicSpendItem[] spendItems; + decimal budgetTotal?; + decimal remainingBudget?; + decimal spendTotal?; + "AED"|"AFN"|"ALL"|"AMD"|"ANG"|"AOA"|"ARS"|"AUD"|"AWG"|"AZN"|"BAM"|"BBD"|"BDT"|"BGN"|"BHD"|"BIF"|"BMD"|"BND"|"BOB"|"BOV"|"BRL"|"BSD"|"BTN"|"BWP"|"BYN"|"BZD"|"CAD"|"CDF"|"CHE"|"CHF"|"CHW"|"CLF"|"CLP"|"CNY"|"COP"|"COU"|"CRC"|"CUC"|"CUP"|"CVE"|"CZK"|"DJF"|"DKK"|"DOP"|"DZD"|"EGP"|"ERN"|"ETB"|"EUR"|"FJD"|"FKP"|"GBP"|"GEL"|"GHS"|"GIP"|"GMD"|"GNF"|"GTQ"|"GYD"|"HKD"|"HNL"|"HRK"|"HTG"|"HUF"|"IDR"|"ILS"|"INR"|"IQD"|"IRR"|"ISK"|"JMD"|"JOD"|"JPY"|"KES"|"KGS"|"KHR"|"KMF"|"KPW"|"KRW"|"KWD"|"KYD"|"KZT"|"LAK"|"LBP"|"LKR"|"LRD"|"LSL"|"LYD"|"MAD"|"MDL"|"MGA"|"MKD"|"MMK"|"MNT"|"MOP"|"MRU"|"MUR"|"MVR"|"MWK"|"MXN"|"MXV"|"MYR"|"MZN"|"NAD"|"NGN"|"NIO"|"NOK"|"NPR"|"NZD"|"OMR"|"PAB"|"PEN"|"PGK"|"PHP"|"PKR"|"PLN"|"PYG"|"QAR"|"RON"|"RSD"|"RUB"|"RWF"|"SAR"|"SBD"|"SCR"|"SDG"|"SEK"|"SGD"|"SHP"|"SLL"|"SOS"|"SRD"|"SSP"|"STN"|"SVC"|"SYP"|"SZL"|"THB"|"TJS"|"TMT"|"TND"|"TOP"|"TRY"|"TTD"|"TWD"|"TZS"|"UAH"|"UGX"|"USD"|"USN"|"UYI"|"UYU"|"UZS"|"VEF"|"VND"|"VUV"|"WST"|"XAF"|"XAG"|"XAU"|"XBA"|"XBB"|"XBC"|"XBD"|"XCD"|"XDR"|"XOF"|"XPD"|"XPF"|"XPT"|"XSU"|"XUA"|"YER"|"ZAR"|"ZMW"|"ZWL" currencyCode; + PublicBudgetItem[] budgetItems; +}; + +public type CollectionResponsePublicCampaignAsset record { + Paging paging?; + PublicCampaignAsset[] results; +}; + +public type PublicCampaignDeleteInput record { + string id; +}; + +public type BatchInputPublicCampaignDeleteInput record { + PublicCampaignDeleteInput[] inputs; +}; + +public type PublicCampaignInput record { + record {|string...;|} properties; +}; + +public type MetricsCounters record { + int:Signed32 sessions; + int:Signed32 newContactsFirstTouch; + int:Signed32 influencedContacts; + int:Signed32 newContactsLastTouch; +}; + +public type PublicBudgetItem record { + int:Signed32 createdAt; + decimal amount; + string name; + string description?; + string id; + int:Signed32 'order; + int:Signed32 updatedAt; +}; + +# Represents the Queries record for the operation: get-/marketing/v3/campaigns/{campaignGuid} +public type GetMarketingV3CampaignsCampaignguidQueries record { + # End date to fetch asset metrics, formatted as YYYY-MM-DD. This date is used to fetch the metrics associated with the assets for a specified period. + # If not provided, no asset metrics will be fetched. + string endDate?; + # Start date to fetch asset metrics, formatted as YYYY-MM-DD. This date is used to fetch the metrics associated with the assets for a specified period. + # If not provided, no asset metrics will be fetched. + string startDate?; + # A comma-separated list of the properties to be returned in the response. If any of the specified properties has empty value on the requested object, they will be ignored and not returned in response. If this parameter is empty, the response will include an empty properties map. + string[] properties?; +}; + +public type BatchInputPublicCampaignInput record { + PublicCampaignInput[] inputs; +}; + +public type CollectionResponseWithTotalPublicCampaignForwardPaging record { + int:Signed32 total; + ForwardPaging paging?; + PublicCampaign[] results; +}; + +public type Paging record { + NextPage next?; + PreviousPage prev?; +}; + +# Proxy server configurations to be used with the HTTP client endpoint. +public type ProxyConfig record {| + # Host name of the proxy server + string host = ""; + # Proxy server port + int port = 0; + # Proxy server username + string userName = ""; + # Proxy server password + @display {label: "", kind: "password"} + string password = ""; +|}; + +# Represents the Queries record for the operation: get-/marketing/v3/campaigns/{campaignGuid}/reports/metrics +public type GetMarketingV3CampaignsCampaignguidReportsMetricsQueries record { + # End date for the report data, formatted as YYYY-MM-DD. + # Default value: Current date + string endDate?; + # The start date for the report data, formatted as YYYY-MM-DD. + # Default value: 2006-01-01 + string startDate?; +}; + +public type CollectionResponseContactReferenceForwardPaging record { + ForwardPaging paging?; + ContactReference[] results; +}; + +# Represents the Queries record for the operation: get-/marketing/v3/campaigns/{campaignGuid}/assets/{assetType} +public type GetMarketingV3CampaignsCampaignguidAssetsAssettypeQueries record { + # End date to fetch asset metrics, formatted as YYYY-MM-DD. This date is used to fetch the metrics associated with the assets for a specified period. + # If not provided, no asset metrics will be fetched. + string endDate?; + # The maximum number of results to return. + # Default: 10 + string 'limit?; + # A cursor for pagination. If provided, the results will start after the given cursor. + # Example: NTI1Cg%3D%3D + string after?; + # Start date to fetch asset metrics, formatted as YYYY-MM-DD. This date is used to fetch the metrics associated with the assets for a specified period. + # If not provided, no asset metrics will be fetched. + string startDate?; +}; + +public type PublicCampaignAsset record { + string name?; + string id; + record {||} metrics?; +}; + +public type RevenueAttributionAggregate record { + int:Signed32 contactsNumber?; + decimal dealAmount?; + int:Signed32 dealsNumber?; + decimal revenueAmount?; + "AED"|"AFN"|"ALL"|"AMD"|"ANG"|"AOA"|"ARS"|"AUD"|"AWG"|"AZN"|"BAM"|"BBD"|"BDT"|"BGN"|"BHD"|"BIF"|"BMD"|"BND"|"BOB"|"BOV"|"BRL"|"BSD"|"BTN"|"BWP"|"BYN"|"BZD"|"CAD"|"CDF"|"CHE"|"CHF"|"CHW"|"CLF"|"CLP"|"CNY"|"COP"|"COU"|"CRC"|"CUC"|"CUP"|"CVE"|"CZK"|"DJF"|"DKK"|"DOP"|"DZD"|"EGP"|"ERN"|"ETB"|"EUR"|"FJD"|"FKP"|"GBP"|"GEL"|"GHS"|"GIP"|"GMD"|"GNF"|"GTQ"|"GYD"|"HKD"|"HNL"|"HRK"|"HTG"|"HUF"|"IDR"|"ILS"|"INR"|"IQD"|"IRR"|"ISK"|"JMD"|"JOD"|"JPY"|"KES"|"KGS"|"KHR"|"KMF"|"KPW"|"KRW"|"KWD"|"KYD"|"KZT"|"LAK"|"LBP"|"LKR"|"LRD"|"LSL"|"LYD"|"MAD"|"MDL"|"MGA"|"MKD"|"MMK"|"MNT"|"MOP"|"MRU"|"MUR"|"MVR"|"MWK"|"MXN"|"MXV"|"MYR"|"MZN"|"NAD"|"NGN"|"NIO"|"NOK"|"NPR"|"NZD"|"OMR"|"PAB"|"PEN"|"PGK"|"PHP"|"PKR"|"PLN"|"PYG"|"QAR"|"RON"|"RSD"|"RUB"|"RWF"|"SAR"|"SBD"|"SCR"|"SDG"|"SEK"|"SGD"|"SHP"|"SLL"|"SOS"|"SRD"|"SSP"|"STN"|"SVC"|"SYP"|"SZL"|"THB"|"TJS"|"TMT"|"TND"|"TOP"|"TRY"|"TTD"|"TWD"|"TZS"|"UAH"|"UGX"|"USD"|"USN"|"UYI"|"UYU"|"UZS"|"VEF"|"VND"|"VUV"|"WST"|"XAF"|"XAG"|"XAU"|"XBA"|"XBB"|"XBC"|"XBD"|"XCD"|"XDR"|"XOF"|"XPD"|"XPF"|"XPT"|"XSU"|"XUA"|"YER"|"ZAR"|"ZMW"|"ZWL" currencyCode?; +}; + +# Represents the Queries record for the operation: get-/marketing/v3/campaigns/{campaignGuid}/reports/contacts/{contactType} +public type GetMarketingV3CampaignsCampaignguidReportsContactsContacttypeQueries record { + # End date for the report data, formatted as YYYY-MM-DD. + # Default value: Current date + string endDate?; + # Limit for the number of contacts to fetch + # Default: 100 + int:Signed32 'limit?; + # A cursor for pagination. If provided, the results will start after the given cursor. + # Example: NTI1Cg%3D%3D + string after?; + # The start date for the report data, formatted as YYYY-MM-DD. + # Default value: 2006-01-01 + string startDate?; +}; + +public type BatchInputPublicCampaignBatchUpdateItem record { + PublicCampaignBatchUpdateItem[] inputs; +}; + +# Provides settings related to HTTP/1.x protocol. +public type ClientHttp1Settings record {| + # Specifies whether to reuse a connection for multiple requests + http:KeepAlive keepAlive = http:KEEPALIVE_AUTO; + # The chunking behaviour of the request + http:Chunking chunking = http:CHUNKING_AUTO; + # Proxy server related options + ProxyConfig proxy?; +|}; + +public type PreviousPage record { + string before; + string link?; +}; + +# Represents the Queries record for the operation: get-/marketing/v3/campaigns/ +public type GetMarketingV3CampaignsQueries record { + # The maximum number of results to return. Allowed values range from 1 to 100 + # Default: 50 + int:Signed32 'limit?; + # A filter to return campaigns whose names contain the specified substring. This allows partial matching of campaign names, returning all campaigns that include the given substring in their name. If this parameter is not provided, the search will return all campaigns + string name?; + # The field by which to sort the results. Allowed values are hs_name, createdAt, updatedAt. An optional '-' before the property name can denote descending order + # Default: hs_name + string sort?; + # A cursor for pagination. If provided, the results will start after the given cursor. + # Example: NTI1Cg%3D%3D + string after?; + # A comma-separated list of the properties to be returned in the response. If any of the specified properties has empty value on the requested object(s), they will be ignored and not returned in response. If this parameter is empty, the response will include an empty properties map + string[] properties?; +}; + +public type ContactReference record { + string id; +}; + +public type NextPage record { + string link?; + string after; +}; + +# Provides API key configurations needed when communicating with a remote HTTP endpoint. +# +# + private\-app - field description +public type ApiKeysConfig record {| + string private\-app; +|}; + +public type PublicCampaign record { + string createdAt; + string id; + record {|string...;|} properties; + string updatedAt; +}; diff --git a/ballerina/utils.bal b/ballerina/utils.bal new file mode 100644 index 0000000..b2aff1f --- /dev/null +++ b/ballerina/utils.bal @@ -0,0 +1,231 @@ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/url; + +type SimpleBasicType string|boolean|int|float|decimal; + +# Represents encoding mechanism details. +type Encoding record { + # Defines how multiple values are delimited + string style = FORM; + # Specifies whether arrays and objects should generate as separate fields + boolean explode = true; + # Specifies the custom content type + string contentType?; + # Specifies the custom headers + map headers?; +}; + +enum EncodingStyle { + DEEPOBJECT, FORM, SPACEDELIMITED, PIPEDELIMITED +} + +final Encoding & readonly defaultEncoding = {}; + +# Serialize the record according to the deepObject style. +# +# + parent - Parent record name +# + anyRecord - Record to be serialized +# + return - Serialized record as a string +isolated function getDeepObjectStyleRequest(string parent, record {} anyRecord) returns string { + string[] recordArray = []; + foreach [string, anydata] [key, value] in anyRecord.entries() { + if value is SimpleBasicType { + recordArray.push(parent + "[" + key + "]" + "=" + getEncodedUri(value.toString())); + } else if value is SimpleBasicType[] { + recordArray.push(getSerializedArray(parent + "[" + key + "]" + "[]", value, DEEPOBJECT, true)); + } else if value is record {} { + string nextParent = parent + "[" + key + "]"; + recordArray.push(getDeepObjectStyleRequest(nextParent, value)); + } else if value is record {}[] { + string nextParent = parent + "[" + key + "]"; + recordArray.push(getSerializedRecordArray(nextParent, value, DEEPOBJECT)); + } + recordArray.push("&"); + } + _ = recordArray.pop(); + return string:'join("", ...recordArray); +} + +# Serialize the record according to the form style. +# +# + parent - Parent record name +# + anyRecord - Record to be serialized +# + explode - Specifies whether arrays and objects should generate separate parameters +# + return - Serialized record as a string +isolated function getFormStyleRequest(string parent, record {} anyRecord, boolean explode = true) returns string { + string[] recordArray = []; + if explode { + foreach [string, anydata] [key, value] in anyRecord.entries() { + if value is SimpleBasicType { + recordArray.push(key, "=", getEncodedUri(value.toString())); + } else if value is SimpleBasicType[] { + recordArray.push(getSerializedArray(key, value, explode = explode)); + } else if value is record {} { + recordArray.push(getFormStyleRequest(parent, value, explode)); + } + recordArray.push("&"); + } + _ = recordArray.pop(); + } else { + foreach [string, anydata] [key, value] in anyRecord.entries() { + if value is SimpleBasicType { + recordArray.push(key, ",", getEncodedUri(value.toString())); + } else if value is SimpleBasicType[] { + recordArray.push(getSerializedArray(key, value, explode = false)); + } else if value is record {} { + recordArray.push(getFormStyleRequest(parent, value, explode)); + } + recordArray.push(","); + } + _ = recordArray.pop(); + } + return string:'join("", ...recordArray); +} + +# Serialize arrays. +# +# + arrayName - Name of the field with arrays +# + anyArray - Array to be serialized +# + style - Defines how multiple values are delimited +# + explode - Specifies whether arrays and objects should generate separate parameters +# + return - Serialized array as a string +isolated function getSerializedArray(string arrayName, anydata[] anyArray, string style = "form", boolean explode = true) returns string { + string key = arrayName; + string[] arrayValues = []; + if anyArray.length() > 0 { + if style == FORM && !explode { + arrayValues.push(key, "="); + foreach anydata i in anyArray { + arrayValues.push(getEncodedUri(i.toString()), ","); + } + } else if style == SPACEDELIMITED && !explode { + arrayValues.push(key, "="); + foreach anydata i in anyArray { + arrayValues.push(getEncodedUri(i.toString()), "%20"); + } + } else if style == PIPEDELIMITED && !explode { + arrayValues.push(key, "="); + foreach anydata i in anyArray { + arrayValues.push(getEncodedUri(i.toString()), "|"); + } + } else if style == DEEPOBJECT { + foreach anydata i in anyArray { + arrayValues.push(key, "[]", "=", getEncodedUri(i.toString()), "&"); + } + } else { + foreach anydata i in anyArray { + arrayValues.push(key, "=", getEncodedUri(i.toString()), "&"); + } + } + _ = arrayValues.pop(); + } + return string:'join("", ...arrayValues); +} + +# Serialize the array of records according to the form style. +# +# + parent - Parent record name +# + value - Array of records to be serialized +# + style - Defines how multiple values are delimited +# + explode - Specifies whether arrays and objects should generate separate parameters +# + return - Serialized record as a string +isolated function getSerializedRecordArray(string parent, record {}[] value, string style = FORM, boolean explode = true) returns string { + string[] serializedArray = []; + if style == DEEPOBJECT { + int arayIndex = 0; + foreach var recordItem in value { + serializedArray.push(getDeepObjectStyleRequest(parent + "[" + arayIndex.toString() + "]", recordItem), "&"); + arayIndex = arayIndex + 1; + } + } else { + if !explode { + serializedArray.push(parent, "="); + } + foreach var recordItem in value { + serializedArray.push(getFormStyleRequest(parent, recordItem, explode), ","); + } + } + _ = serializedArray.pop(); + return string:'join("", ...serializedArray); +} + +# Get Encoded URI for a given value. +# +# + value - Value to be encoded +# + return - Encoded string +isolated function getEncodedUri(anydata value) returns string { + string|error encoded = url:encode(value.toString(), "UTF8"); + if encoded is string { + return encoded; + } else { + return value.toString(); + } +} + +# Generate query path with query parameter. +# +# + queryParam - Query parameter map +# + encodingMap - Details on serialization mechanism +# + return - Returns generated Path or error at failure of client initialization +isolated function getPathForQueryParam(map queryParam, map encodingMap = {}) returns string|error { + string[] param = []; + if queryParam.length() > 0 { + param.push("?"); + foreach var [key, value] in queryParam.entries() { + if value is () { + _ = queryParam.remove(key); + continue; + } + Encoding encodingData = encodingMap.hasKey(key) ? encodingMap.get(key) : defaultEncoding; + if value is SimpleBasicType { + param.push(key, "=", getEncodedUri(value.toString())); + } else if value is SimpleBasicType[] { + param.push(getSerializedArray(key, value, encodingData.style, encodingData.explode)); + } else if value is record {} { + if encodingData.style == DEEPOBJECT { + param.push(getDeepObjectStyleRequest(key, value)); + } else { + param.push(getFormStyleRequest(key, value, encodingData.explode)); + } + } else { + param.push(key, "=", value.toString()); + } + param.push("&"); + } + _ = param.pop(); + } + string restOfPath = string:'join("", ...param); + return restOfPath; +} + +# Generate header map for given header values. +# +# + headerParam - Headers map +# + return - Returns generated map or error at failure of client initialization +isolated function getMapForHeaders(map headerParam) returns map { + map headerMap = {}; + foreach var [key, value] in headerParam.entries() { + if value is SimpleBasicType[] { + headerMap[key] = from SimpleBasicType data in value + select data.toString(); + } else { + headerMap[key] = value.toString(); + } + } + return headerMap; +} diff --git a/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml index 54b26ea..3ad8554 100644 --- a/build-config/resources/Ballerina.toml +++ b/build-config/resources/Ballerina.toml @@ -5,12 +5,12 @@ name = "hubspot.marketing.campaigns" version = "@toml.version@" license = ["Apache-2.0"] authors = ["Ballerina"] -keywords = [] # TODO: Add keywords -# icon = "icon.png" # TODO: Add icon +keywords = ["hubspot", "crm", "marketing", "campaigns"] + repository = "https://github.com/ballerina-platform/module-ballerinax-hubspot.marketing.campaigns" [build-options] observabilityIncluded = true -[platform.java21] +[platform.java17] graalvmCompatible = true diff --git a/docs/resources/accountSelect.png b/docs/resources/accountSelect.png new file mode 100644 index 0000000..b6b7973 Binary files /dev/null and b/docs/resources/accountSelect.png differ diff --git a/docs/resources/appSection.png b/docs/resources/appSection.png new file mode 100644 index 0000000..9c2c074 Binary files /dev/null and b/docs/resources/appSection.png differ diff --git a/docs/resources/auth.png b/docs/resources/auth.png new file mode 100644 index 0000000..c417dc3 Binary files /dev/null and b/docs/resources/auth.png differ diff --git a/docs/resources/clientId_secretId.png b/docs/resources/clientId_secretId.png new file mode 100644 index 0000000..cf552c1 Binary files /dev/null and b/docs/resources/clientId_secretId.png differ diff --git a/docs/resources/createAccount.png b/docs/resources/createAccount.png new file mode 100644 index 0000000..b54a44d Binary files /dev/null and b/docs/resources/createAccount.png differ diff --git a/docs/resources/developmentTestAccount.png b/docs/resources/developmentTestAccount.png new file mode 100644 index 0000000..ebcfe48 Binary files /dev/null and b/docs/resources/developmentTestAccount.png differ diff --git a/docs/resources/marketingScopes.png b/docs/resources/marketingScopes.png new file mode 100644 index 0000000..7b65455 Binary files /dev/null and b/docs/resources/marketingScopes.png differ diff --git a/docs/resources/namingApp.png b/docs/resources/namingApp.png new file mode 100644 index 0000000..4f81e71 Binary files /dev/null and b/docs/resources/namingApp.png differ diff --git a/docs/resources/redirectURL.png b/docs/resources/redirectURL.png new file mode 100644 index 0000000..87ef789 Binary files /dev/null and b/docs/resources/redirectURL.png differ diff --git a/docs/resources/testAccount.png b/docs/resources/testAccount.png new file mode 100644 index 0000000..18b16b2 Binary files /dev/null and b/docs/resources/testAccount.png differ diff --git a/docs/resources/testAccountPortal.png b/docs/resources/testAccountPortal.png new file mode 100644 index 0000000..32216e7 Binary files /dev/null and b/docs/resources/testAccountPortal.png differ diff --git a/docs/spec/campaignsPublicApi.json b/docs/spec/campaignsPublicApi.json new file mode 100644 index 0000000..8dc37bb --- /dev/null +++ b/docs/spec/campaignsPublicApi.json @@ -0,0 +1,1713 @@ +{ + "openapi" : "3.0.1", + "info" : { + "title" : "Marketing Campaigns Public Api", + "version" : "v3", + "x-hubspot-product-tier-requirements" : { + "marketing" : "PROFESSIONAL" + }, + "x-hubspot-documentation-banner" : "NONE", + "x-hubspot-related-documentation" : [ { + "name" : "Campaigns", + "url" : "https://developers.hubspot.com/beta-docs/guides/api/marketing/campaigns" + } ], + "x-hubspot-introduction" : "You can use campaign endpoints to create, read, update, and delete marketing campaign data. For example, you can use these endpoints to create and manage HubSpot campaigns from your external applications. You can also transfer campaign data to external data warehouses for analytics." + }, + "servers" : [ { + "url" : "https://api.hubapi.com/marketing/v3/campaigns" + } ], + "tags" : [ { + "name" : "Basic" + }, { + "name" : "Search" + }, { + "name" : "Batch" + }, { + "name" : "Reports" + }, { + "name" : "Asset" + }, { + "name" : "Budget" + } ], + "paths" : { + "/" : { + "get" : { + "tags" : [ "Search" ], + "summary" : "Campaign search", + "description" : "This endpoint allows users to search for and return a page of campaigns based on various query parameters. Users can filter by name, sort, and paginate through the campaigns, as well as control which properties are returned in the response.", + "operationId" : "get-/marketing/v3/campaigns/", + "parameters" : [ { + "name" : "sort", + "in" : "query", + "description" : "The field by which to sort the results. Allowed values are hs_name, createdAt, updatedAt. An optional '-' before the property name can denote descending order\nDefault: hs_name", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "after", + "in" : "query", + "description" : "A cursor for pagination. If provided, the results will start after the given cursor.\nExample: NTI1Cg%3D%3D", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "limit", + "in" : "query", + "description" : "The maximum number of results to return. Allowed values range from 1 to 100\nDefault: 50", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "integer", + "format" : "int32" + } + }, { + "name" : "name", + "in" : "query", + "description" : "A filter to return campaigns whose names contain the specified substring. This allows partial matching of campaign names, returning all campaigns that include the given substring in their name. If this parameter is not provided, the search will return all campaigns", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "properties", + "in" : "query", + "description" : "A comma-separated list of the properties to be returned in the response. If any of the specified properties has empty value on the requested object(s), they will be ignored and not returned in response. If this parameter is empty, the response will include an empty properties map", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "array", + "items" : { + "type" : "string" + } + } + } ], + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CollectionResponseWithTotalPublicCampaignForwardPaging" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "marketing.campaigns.read" ] + }, { + "private_apps" : [ "marketing.campaigns.read" ] + } ] + }, + "post" : { + "tags" : [ "Basic" ], + "summary" : "Create a campaign", + "description" : "Create a campaign with the given properties and return the campaign object, including the campaignGuid and created properties. ", + "operationId" : "post-/marketing/v3/campaigns/", + "parameters" : [ ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PublicCampaignInput" + } + } + }, + "required" : true + }, + "responses" : { + "201" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PublicCampaign" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "marketing.campaigns.read" ] + }, { + "private_apps" : [ "marketing.campaigns.read" ] + } ] + } + }, + "/batch/read" : { + "post" : { + "tags" : [ "Batch" ], + "summary" : "Read a batch of campaigns", + "description" : "This endpoint reads a batch of campaigns based on the provided input data and returns the campaigns along with their associated assets. \nThe maximum number of items in a batch request is 50.\nThe campaigns in the response are not guaranteed to be in the same order as they were provided in the request.\nIf duplicate campaign IDs are provided in the request, duplicates will be ignored. The response will include only unique IDs and will be returned without duplicates.\n", + "operationId" : "post-/marketing/v3/campaigns/batch/read", + "parameters" : [ { + "name" : "startDate", + "in" : "query", + "description" : "Start date to fetch asset metrics, formatted as YYYY-MM-DD. This date is used to fetch the metrics associated with the assets for a specified period. If not provided, no asset metrics will be fetched.\n", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "endDate", + "in" : "query", + "description" : "End date to fetch asset metrics, formatted as YYYY-MM-DD. This date is used to fetch the metrics associated with the assets for a specified period. If not provided, no asset metrics will be fetched.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "properties", + "in" : "query", + "description" : "A comma-separated list of the properties to be returned in the response. If any of the specified properties has empty value on the requested object(s), they will be ignored and not returned in response. If this parameter is empty, the response will include an empty properties map.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "array", + "items" : { + "type" : "string" + } + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchInputPublicCampaignReadInput" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchResponsePublicCampaignWithAssets" + } + } + } + }, + "207" : { + "description" : "multiple statuses", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchResponsePublicCampaignWithAssetsWithErrors" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "marketing.campaigns.read" ] + }, { + "private_apps" : [ "marketing.campaigns.read" ] + } ] + } + }, + "/batch/update" : { + "post" : { + "tags" : [ "Batch" ], + "summary" : "Update a batch of campaigns", + "description" : "This endpoint updates a batch of campaigns based on the provided input data.\nThe maximum number of items in a batch request is 50.\nIf an empty string (\"\") is passed for any property in the Batch Update, it will reset that property's value.", + "operationId" : "post-/marketing/v3/campaigns/batch/update", + "parameters" : [ ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchInputPublicCampaignBatchUpdateItem" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchResponsePublicCampaign" + } + } + } + }, + "207" : { + "description" : "multiple statuses", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchResponsePublicCampaignWithErrors" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "marketing.campaigns.read" ] + }, { + "private_apps" : [ "marketing.campaigns.read" ] + } ] + } + }, + "/{campaignGuid}/reports/metrics" : { + "get" : { + "tags" : [ "Reports" ], + "summary" : "Get Campaign Metrics\n", + "description" : "This endpoint retrieves key attribution metrics for a specified campaign, such as sessions, new contacts, and influenced contacts.", + "operationId" : "get-/marketing/v3/campaigns/{campaignGuid}/reports/metrics", + "parameters" : [ { + "name" : "campaignGuid", + "in" : "path", + "description" : "Unique identifier for the campaign, formatted as a UUID.", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + }, { + "name" : "startDate", + "in" : "query", + "description" : "The start date for the report data, formatted as YYYY-MM-DD.\nDefault value: 2006-01-01", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "endDate", + "in" : "query", + "description" : "End date for the report data, formatted as YYYY-MM-DD.\nDefault value: Current date", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/MetricsCounters" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "marketing.campaigns.read" ] + }, { + "private_apps" : [ "marketing.campaigns.read" ] + } ] + } + }, + "/{campaignGuid}/assets/{assetType}" : { + "get" : { + "tags" : [ "Asset" ], + "summary" : "List assets", + "description" : "This endpoint lists all assets of the campaign by asset type. The assetType parameter is required, and each request can only fetch assets of a single type.\nAsset metrics can also be fetched along with the assets; they are available only if start and end dates are provided.", + "operationId" : "get-/marketing/v3/campaigns/{campaignGuid}/assets/{assetType}", + "parameters" : [ { + "name" : "campaignGuid", + "in" : "path", + "description" : "Unique identifier for the campaign, formatted as a UUID.", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + }, { + "name" : "assetType", + "in" : "path", + "description" : "The type of asset to fetch.", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + }, { + "name" : "after", + "in" : "query", + "description" : "A cursor for pagination. If provided, the results will start after the given cursor.\nExample: NTI1Cg%3D%3D", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "limit", + "in" : "query", + "description" : "The maximum number of results to return.\nDefault: 10", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "startDate", + "in" : "query", + "description" : "Start date to fetch asset metrics, formatted as YYYY-MM-DD. This date is used to fetch the metrics associated with the assets for a specified period.\nIf not provided, no asset metrics will be fetched.\n", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "endDate", + "in" : "query", + "description" : "End date to fetch asset metrics, formatted as YYYY-MM-DD. This date is used to fetch the metrics associated with the assets for a specified period.\nIf not provided, no asset metrics will be fetched.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CollectionResponsePublicCampaignAssetForwardPaging" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "marketing.campaigns.read" ] + }, { + "private_apps" : [ "marketing.campaigns.read" ] + } ] + } + }, + "/batch/archive" : { + "post" : { + "tags" : [ "Batch" ], + "summary" : "Delete a batch of campaigns", + "description" : "This endpoint deletes a batch of campaigns. \nThe maximum number of items in a batch request is 50.\nThe response will always be 204 No Content, regardless of whether the campaigns exist or not, whether they were successfully deleted or not, or if only some of the campaigns in the batch were deleted.", + "operationId" : "post-/marketing/v3/campaigns/batch/archive", + "parameters" : [ ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchInputPublicCampaignDeleteInput" + } + } + }, + "required" : true + }, + "responses" : { + "204" : { + "description" : "No content", + "content" : { } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "marketing.campaigns.read" ] + }, { + "private_apps" : [ "marketing.campaigns.read" ] + } ] + } + }, + "/{campaignGuid}/assets/{assetType}/{assetId}" : { + "put" : { + "tags" : [ "Asset" ], + "summary" : "Add asset association", + "description" : "Associate a specified asset with a campaign.\nImportant: Currently, only the following asset types can be associated and disassociated via the API: Forms, Static lists, External website pages", + "operationId" : "put-/marketing/v3/campaigns/{campaignGuid}/assets/{assetType}/{assetId}", + "parameters" : [ { + "name" : "campaignGuid", + "in" : "path", + "description" : "Unique identifier for the campaign, formatted as a UUID", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + }, { + "name" : "assetType", + "in" : "path", + "description" : "The type of asset\nImportant: Currently, only the following asset types are available for association via the API: FORM, OBJECT_LIST, EXTERNAL_WEB_URL", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + }, { + "name" : "assetId", + "in" : "path", + "description" : "Id of the asset", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "204" : { + "description" : "No content", + "content" : { } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "marketing.campaigns.read" ] + }, { + "private_apps" : [ "marketing.campaigns.read" ] + } ] + }, + "delete" : { + "tags" : [ "Asset" ], + "summary" : "Remove asset association", + "description" : "Disassociate a specified asset from a campaign.\nImportant: Currently, only the following asset types can be associated and disassociated via the API: Forms, Static lists, External website pages", + "operationId" : "delete-/marketing/v3/campaigns/{campaignGuid}/assets/{assetType}/{assetId}", + "parameters" : [ { + "name" : "campaignGuid", + "in" : "path", + "description" : "Unique identifier for the campaign, formatted as a UUID.", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + }, { + "name" : "assetType", + "in" : "path", + "description" : "The type of asset\nImportant: Currently, only the following asset types are available for disassociation via the API: FORM, OBJECT_LIST, EXTERNAL_WEB_URL", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + }, { + "name" : "assetId", + "in" : "path", + "description" : "Id of the asset", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "204" : { + "description" : "No content", + "content" : { } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "marketing.campaigns.read" ] + }, { + "private_apps" : [ "marketing.campaigns.read" ] + } ] + } + }, + "/{campaignGuid}/reports/revenue" : { + "get" : { + "tags" : [ "Reports" ], + "summary" : "Fetch revenue", + "description" : "Fetch revenue attribution report data for a specified campaign\n", + "operationId" : "get-/marketing/v3/campaigns/{campaignGuid}/reports/revenue", + "parameters" : [ { + "name" : "campaignGuid", + "in" : "path", + "description" : "Unique identifier for the campaign, formatted as a UUID.", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + }, { + "name" : "attributionModel", + "in" : "query", + "description" : "Allowed values: LINEAR, FIRST_INTERACTION, LAST_INTERACTION, FULL_PATH, U_SHAPED, W_SHAPED, TIME_DECAY, J_SHAPED, INVERSE_J_SHAPED\nDefault value: LINEAR", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "startDate", + "in" : "query", + "description" : "The start date for the report data, formatted as YYYY-MM-DD.\nDefault value: 2006-01-01", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "endDate", + "in" : "query", + "description" : "End date for the report data, formatted as YYYY-MM-DD.\nDefault value: Current date", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RevenueAttributionAggregate" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "private_apps" : [ "marketing.campaigns.revenue.read" ] + }, { + "oauth2" : [ "marketing.campaigns.revenue.read" ] + } ] + } + }, + "/batch/create" : { + "post" : { + "tags" : [ "Batch" ], + "summary" : "Create a batch of campaigns", + "description" : "This endpoint creates a batch of campaigns. The maximum number of items in a batch request is 50.\nThe campaigns in the response are not guaranteed to be in the same order as they were provided in the request.", + "operationId" : "post-/marketing/v3/campaigns/batch/create", + "parameters" : [ ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchInputPublicCampaignInput" + } + } + }, + "required" : true + }, + "responses" : { + "201" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchResponsePublicCampaign" + } + } + } + }, + "207" : { + "description" : "multiple statuses", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchResponsePublicCampaignWithErrors" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "marketing.campaigns.read" ] + }, { + "private_apps" : [ "marketing.campaigns.read" ] + } ] + } + }, + "/{campaignGuid}/budget/totals" : { + "get" : { + "tags" : [ "Budget" ], + "summary" : "Read budget", + "description" : "Retrieve detailed information about the budget and spend items for a specified campaign, including the total budget, total spend, and remaining budget.\nBudget and Spend items may be returned in any order, but the order field specifies their sequence based on the creation date. The item with order 0 is the oldest, and items with higher order values are newer", + "operationId" : "get-/marketing/v3/campaigns/{campaignGuid}/budget/totals", + "parameters" : [ { + "name" : "campaignGuid", + "in" : "path", + "description" : "Unique identifier for the campaign, formatted as a UUID.", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PublicBudgetTotals" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "marketing.campaigns.read" ] + }, { + "private_apps" : [ "marketing.campaigns.read" ] + } ] + } + }, + "/{campaignGuid}" : { + "get" : { + "tags" : [ "Basic" ], + "summary" : "Read a campaign", + "description" : "Get a campaign identified by a specific campaignGuid with the given properties. Along with the campaign information, it also returns information about assets. Depending on the query parameters used, this can also be used to return information about the corresponding assets' metrics. Metrics are available only if startDate and endDate are provided.", + "operationId" : "get-/marketing/v3/campaigns/{campaignGuid}", + "parameters" : [ { + "name" : "campaignGuid", + "in" : "path", + "description" : "Unique identifier for the campaign, formatted as a UUID.", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + }, { + "name" : "startDate", + "in" : "query", + "description" : "Start date to fetch asset metrics, formatted as YYYY-MM-DD. This date is used to fetch the metrics associated with the assets for a specified period.\nIf not provided, no asset metrics will be fetched.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "endDate", + "in" : "query", + "description" : " End date to fetch asset metrics, formatted as YYYY-MM-DD. This date is used to fetch the metrics associated with the assets for a specified period.\nIf not provided, no asset metrics will be fetched.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "properties", + "in" : "query", + "description" : "A comma-separated list of the properties to be returned in the response. If any of the specified properties has empty value on the requested object, they will be ignored and not returned in response. If this parameter is empty, the response will include an empty properties map.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "array", + "items" : { + "type" : "string" + } + } + } ], + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PublicCampaignWithAssets" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "marketing.campaigns.read" ] + }, { + "private_apps" : [ "marketing.campaigns.read" ] + } ] + }, + "delete" : { + "tags" : [ "Basic" ], + "summary" : "Delete campaign ", + "description" : "Delete a specified campaign from the system.\nThis call will return a 204 No Content response regardless of whether the campaignGuid provided corresponds to an existing campaign or not.", + "operationId" : "delete-/marketing/v3/campaigns/{campaignGuid}", + "parameters" : [ { + "name" : "campaignGuid", + "in" : "path", + "description" : "Unique identifier for the campaign, formatted as a UUID.", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "204" : { + "description" : "No content", + "content" : { } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "marketing.campaigns.read" ] + }, { + "private_apps" : [ "marketing.campaigns.read" ] + } ] + }, + "patch" : { + "tags" : [ "Basic" ], + "summary" : "Update campaign", + "description" : "Perform a partial update of a campaign identified by the specified campaignGuid. Provided property values will be overwritten. Read-only and non-existent properties will cause 400 error.\nIf an empty string is passed for any property in the Batch Update, it will reset that property's value.\n", + "operationId" : "patch-/marketing/v3/campaigns/{campaignGuid}", + "parameters" : [ { + "name" : "campaignGuid", + "in" : "path", + "description" : "Unique identifier for the campaign, formatted as a UUID.\n", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PublicCampaignInput" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PublicCampaign" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "marketing.campaigns.read" ] + }, { + "private_apps" : [ "marketing.campaigns.read" ] + } ] + } + }, + "/{campaignGuid}/reports/contacts/{contactType}" : { + "get" : { + "tags" : [ "Reports" ], + "summary" : "Fetch contact IDs", + "description" : "Fetch the list of contact IDs for the specified campaign and contact type", + "operationId" : "get-/marketing/v3/campaigns/{campaignGuid}/reports/contacts/{contactType}", + "parameters" : [ { + "name" : "campaignGuid", + "in" : "path", + "description" : "Unique identifier for the campaign, formatted as a UUID.", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + }, { + "name" : "contactType", + "in" : "path", + "description" : "The type of metric to filter the influenced contacts. Allowed values: contactFirstTouch, contactLastTouch, influencedContacts", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + }, { + "name" : "startDate", + "in" : "query", + "description" : "The start date for the report data, formatted as YYYY-MM-DD.\nDefault value: 2006-01-01", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "endDate", + "in" : "query", + "description" : "End date for the report data, formatted as YYYY-MM-DD.\nDefault value: Current date", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "limit", + "in" : "query", + "description" : "Limit for the number of contacts to fetch\nDefault: 100", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "integer", + "format" : "int32" + } + }, { + "name" : "after", + "in" : "query", + "description" : "A cursor for pagination. If provided, the results will start after the given cursor.\nExample: NTI1Cg%3D%3D", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CollectionResponseContactReferenceForwardPaging" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "marketing.campaigns.read" ] + }, { + "private_apps" : [ "marketing.campaigns.read" ] + } ] + } + } + }, + "components" : { + "schemas" : { + "StandardError" : { + "required" : [ "category", "context", "errors", "links", "message", "status" ], + "type" : "object", + "properties" : { + "subCategory" : { + "type" : "object", + "properties" : { } + }, + "context" : { + "type" : "object", + "additionalProperties" : { + "type" : "array", + "items" : { + "type" : "string" + } + } + }, + "links" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "id" : { + "type" : "string" + }, + "category" : { + "type" : "string" + }, + "message" : { + "type" : "string" + }, + "errors" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/ErrorDetail" + } + }, + "status" : { + "type" : "string" + } + } + }, + "PublicCampaignBatchUpdateItem" : { + "required" : [ "id", "properties" ], + "type" : "object", + "properties" : { + "id" : { + "type" : "string" + }, + "properties" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + } + } + }, + "CollectionResponsePublicCampaignAssetForwardPaging" : { + "required" : [ "results" ], + "type" : "object", + "properties" : { + "paging" : { + "$ref" : "#/components/schemas/ForwardPaging" + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PublicCampaignAsset" + } + } + } + }, + "PublicCampaignReadInput" : { + "required" : [ "id" ], + "type" : "object", + "properties" : { + "id" : { + "type" : "string" + } + } + }, + "BatchResponsePublicCampaignWithAssetsWithErrors" : { + "required" : [ "completedAt", "results", "startedAt", "status" ], + "type" : "object", + "properties" : { + "completedAt" : { + "type" : "string", + "format" : "date-time" + }, + "numErrors" : { + "type" : "integer", + "format" : "int32" + }, + "requestedAt" : { + "type" : "string", + "format" : "date-time" + }, + "startedAt" : { + "type" : "string", + "format" : "date-time" + }, + "links" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PublicCampaignWithAssets" + } + }, + "errors" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/StandardError" + } + }, + "status" : { + "type" : "string", + "enum" : [ "PENDING", "PROCESSING", "CANCELED", "COMPLETE" ] + } + } + }, + "BatchInputPublicCampaignReadInput" : { + "required" : [ "inputs" ], + "type" : "object", + "properties" : { + "inputs" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PublicCampaignReadInput" + } + } + } + }, + "BatchResponsePublicCampaignWithAssets" : { + "required" : [ "completedAt", "results", "startedAt", "status" ], + "type" : "object", + "properties" : { + "completedAt" : { + "type" : "string", + "format" : "date-time" + }, + "requestedAt" : { + "type" : "string", + "format" : "date-time" + }, + "startedAt" : { + "type" : "string", + "format" : "date-time" + }, + "links" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PublicCampaignWithAssets" + } + }, + "status" : { + "type" : "string", + "enum" : [ "PENDING", "PROCESSING", "CANCELED", "COMPLETE" ] + } + } + }, + "BatchResponsePublicCampaignWithErrors" : { + "required" : [ "completedAt", "results", "startedAt", "status" ], + "type" : "object", + "properties" : { + "completedAt" : { + "type" : "string", + "format" : "date-time" + }, + "numErrors" : { + "type" : "integer", + "format" : "int32" + }, + "requestedAt" : { + "type" : "string", + "format" : "date-time" + }, + "startedAt" : { + "type" : "string", + "format" : "date-time" + }, + "links" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PublicCampaign" + } + }, + "errors" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/StandardError" + } + }, + "status" : { + "type" : "string", + "enum" : [ "PENDING", "PROCESSING", "CANCELED", "COMPLETE" ] + } + } + }, + "ErrorDetail" : { + "required" : [ "message" ], + "type" : "object", + "properties" : { + "subCategory" : { + "type" : "string", + "description" : "A specific category that contains more specific detail about the error" + }, + "code" : { + "type" : "string", + "description" : "The status code associated with the error detail" + }, + "in" : { + "type" : "string", + "description" : "The name of the field or parameter in which the error was found." + }, + "context" : { + "type" : "object", + "additionalProperties" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "description" : "Context about the error condition", + "example" : { + "missingScopes" : [ "scope1", "scope2" ] + } + }, + "message" : { + "type" : "string", + "description" : "A human readable message describing the error along with remediation steps where appropriate" + } + } + }, + "ForwardPaging" : { + "type" : "object", + "properties" : { + "next" : { + "$ref" : "#/components/schemas/NextPage" + } + } + }, + "PublicSpendItem" : { + "required" : [ "amount", "createdAt", "id", "name", "order", "updatedAt" ], + "type" : "object", + "properties" : { + "createdAt" : { + "type" : "integer", + "format" : "int32" + }, + "amount" : { + "type" : "number" + }, + "name" : { + "type" : "string" + }, + "description" : { + "type" : "string" + }, + "id" : { + "type" : "string" + }, + "order" : { + "type" : "integer", + "format" : "int32" + }, + "updatedAt" : { + "type" : "integer", + "format" : "int32" + } + } + }, + "BatchResponsePublicCampaign" : { + "required" : [ "completedAt", "results", "startedAt", "status" ], + "type" : "object", + "properties" : { + "completedAt" : { + "type" : "string", + "format" : "date-time" + }, + "requestedAt" : { + "type" : "string", + "format" : "date-time" + }, + "startedAt" : { + "type" : "string", + "format" : "date-time" + }, + "links" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PublicCampaign" + } + }, + "status" : { + "type" : "string", + "enum" : [ "PENDING", "PROCESSING", "CANCELED", "COMPLETE" ] + } + } + }, + "PublicCampaignWithAssets" : { + "required" : [ "assets", "createdAt", "id", "properties", "updatedAt" ], + "type" : "object", + "properties" : { + "createdAt" : { + "type" : "string", + "format" : "date-time" + }, + "assets" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "#/components/schemas/CollectionResponsePublicCampaignAsset" + } + }, + "id" : { + "type" : "string" + }, + "properties" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "updatedAt" : { + "type" : "string", + "format" : "date-time" + } + } + }, + "PublicBudgetTotals" : { + "required" : [ "budgetItems", "currencyCode", "spendItems" ], + "type" : "object", + "properties" : { + "spendItems" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PublicSpendItem" + } + }, + "budgetTotal" : { + "type" : "number" + }, + "remainingBudget" : { + "type" : "number" + }, + "spendTotal" : { + "type" : "number" + }, + "currencyCode" : { + "type" : "string", + "enum" : [ "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD", "CAD", "CDF", "CHE", "CHF", "CHW", "CLF", "CLP", "CNY", "COP", "COU", "CRC", "CUC", "CUP", "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR", "ILS", "INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", "SOS", "SRD", "SSP", "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "USN", "UYI", "UYU", "UZS", "VEF", "VND", "VUV", "WST", "XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XDR", "XOF", "XPD", "XPF", "XPT", "XSU", "XUA", "YER", "ZAR", "ZMW", "ZWL" ] + }, + "budgetItems" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PublicBudgetItem" + } + } + } + }, + "BatchInputPublicCampaignDeleteInput" : { + "required" : [ "inputs" ], + "type" : "object", + "properties" : { + "inputs" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PublicCampaignDeleteInput" + } + } + } + }, + "CollectionResponsePublicCampaignAsset" : { + "required" : [ "results" ], + "type" : "object", + "properties" : { + "paging" : { + "$ref" : "#/components/schemas/Paging" + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PublicCampaignAsset" + } + } + } + }, + "PublicCampaignDeleteInput" : { + "required" : [ "id" ], + "type" : "object", + "properties" : { + "id" : { + "type" : "string" + } + } + }, + "PublicCampaignInput" : { + "required" : [ "properties" ], + "type" : "object", + "properties" : { + "properties" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + } + } + }, + "MetricsCounters" : { + "required" : [ "influencedContacts", "newContactsFirstTouch", "newContactsLastTouch", "sessions" ], + "type" : "object", + "properties" : { + "sessions" : { + "type" : "integer", + "format" : "int32" + }, + "newContactsFirstTouch" : { + "type" : "integer", + "format" : "int32" + }, + "influencedContacts" : { + "type" : "integer", + "format" : "int32" + }, + "newContactsLastTouch" : { + "type" : "integer", + "format" : "int32" + } + } + }, + "PublicBudgetItem" : { + "required" : [ "amount", "createdAt", "id", "name", "order", "updatedAt" ], + "type" : "object", + "properties" : { + "createdAt" : { + "type" : "integer", + "format" : "int32" + }, + "amount" : { + "type" : "number" + }, + "name" : { + "type" : "string" + }, + "description" : { + "type" : "string" + }, + "id" : { + "type" : "string" + }, + "order" : { + "type" : "integer", + "format" : "int32" + }, + "updatedAt" : { + "type" : "integer", + "format" : "int32" + } + } + }, + "BatchInputPublicCampaignInput" : { + "required" : [ "inputs" ], + "type" : "object", + "properties" : { + "inputs" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PublicCampaignInput" + } + } + } + }, + "CollectionResponseWithTotalPublicCampaignForwardPaging" : { + "required" : [ "results", "total" ], + "type" : "object", + "properties" : { + "total" : { + "type" : "integer", + "format" : "int32" + }, + "paging" : { + "$ref" : "#/components/schemas/ForwardPaging" + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PublicCampaign" + } + } + } + }, + "Paging" : { + "type" : "object", + "properties" : { + "next" : { + "$ref" : "#/components/schemas/NextPage" + }, + "prev" : { + "$ref" : "#/components/schemas/PreviousPage" + } + } + }, + "Error" : { + "required" : [ "category", "correlationId", "message" ], + "type" : "object", + "properties" : { + "subCategory" : { + "type" : "string", + "description" : "A specific category that contains more specific detail about the error" + }, + "context" : { + "type" : "object", + "additionalProperties" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "description" : "Context about the error condition", + "example" : { + "missingScopes" : [ "scope1", "scope2" ], + "invalidPropertyName" : [ "propertyValue" ] + } + }, + "correlationId" : { + "type" : "string", + "description" : "A unique identifier for the request. Include this value with any error reports or support tickets", + "format" : "uuid", + "example" : "aeb5f871-7f07-4993-9211-075dc63e7cbf" + }, + "links" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + }, + "description" : "A map of link names to associated URIs containing documentation about the error or recommended remediation steps" + }, + "message" : { + "type" : "string", + "description" : "A human readable message describing the error along with remediation steps where appropriate", + "example" : "An error occurred" + }, + "category" : { + "type" : "string", + "description" : "The error category" + }, + "errors" : { + "type" : "array", + "description" : "further information about the error", + "items" : { + "$ref" : "#/components/schemas/ErrorDetail" + } + } + }, + "example" : { + "message" : "Invalid input (details will vary based on the error)", + "correlationId" : "aeb5f871-7f07-4993-9211-075dc63e7cbf", + "category" : "VALIDATION_ERROR", + "links" : { + "knowledge-base" : "https://www.hubspot.com/products/service/knowledge-base" + } + } + }, + "CollectionResponseContactReferenceForwardPaging" : { + "required" : [ "results" ], + "type" : "object", + "properties" : { + "paging" : { + "$ref" : "#/components/schemas/ForwardPaging" + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/ContactReference" + } + } + } + }, + "PublicCampaignAsset" : { + "required" : [ "id"], + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + }, + "id" : { + "type" : "string" + }, + "metrics" : { + "type" : "object", + "additionalProperties" : { + "type" : "number" + } + } + } + }, + "RevenueAttributionAggregate" : { + "type" : "object", + "properties" : { + "contactsNumber" : { + "type" : "integer", + "format" : "int32" + }, + "dealAmount" : { + "type" : "number" + }, + "dealsNumber" : { + "type" : "integer", + "format" : "int32" + }, + "revenueAmount" : { + "type" : "number" + }, + "currencyCode" : { + "type" : "string", + "enum" : [ "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD", "CAD", "CDF", "CHE", "CHF", "CHW", "CLF", "CLP", "CNY", "COP", "COU", "CRC", "CUC", "CUP", "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR", "ILS", "INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", "SOS", "SRD", "SSP", "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "USN", "UYI", "UYU", "UZS", "VEF", "VND", "VUV", "WST", "XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XDR", "XOF", "XPD", "XPF", "XPT", "XSU", "XUA", "YER", "ZAR", "ZMW", "ZWL" ] + } + } + }, + "BatchInputPublicCampaignBatchUpdateItem" : { + "required" : [ "inputs" ], + "type" : "object", + "properties" : { + "inputs" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PublicCampaignBatchUpdateItem" + } + } + } + }, + "PreviousPage" : { + "required" : [ "before" ], + "type" : "object", + "properties" : { + "before" : { + "type" : "string" + }, + "link" : { + "type" : "string" + } + } + }, + "ContactReference" : { + "required" : [ "id" ], + "type" : "object", + "properties" : { + "id" : { + "type" : "string" + } + } + }, + "NextPage" : { + "required" : [ "after" ], + "type" : "object", + "properties" : { + "link" : { + "type" : "string" + }, + "after" : { + "type" : "string" + } + } + }, + "PublicCampaign" : { + "required" : [ "createdAt", "id", "properties", "updatedAt" ], + "type" : "object", + "properties" : { + "createdAt" : { + "type" : "string", + "format" : "date-time" + }, + "id" : { + "type" : "string" + }, + "properties" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "updatedAt" : { + "type" : "string", + "format" : "date-time" + } + } + } + }, + "responses" : { + "Error" : { + "description" : "An error occurred.", + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/Error" + } + } + } + } + }, + "securitySchemes" : { + "oauth2" : { + "type" : "oauth2", + "flows" : { + "authorizationCode" : { + "authorizationUrl" : "https://app.hubspot.com/oauth/authorize", + "tokenUrl" : "https://api.hubapi.com/oauth/v1/token", + "scopes" : { + "marketing.campaigns.read" : "Read marketing campaigns", + "marketing.campaigns.revenue.read" : "Read campaigns attribution revenue" + } + } + } + }, + "private_apps" : { + "type" : "apiKey", + "name" : "private-app", + "in" : "header" + } + } + } +} \ No newline at end of file diff --git a/docs/spec/sanitations.md b/docs/spec/sanitations.md index 7036be3..c92340e 100644 --- a/docs/spec/sanitations.md +++ b/docs/spec/sanitations.md @@ -1,24 +1,94 @@ -_Author_: \ -_Created_: \ -_Updated_: \ +_Author_: Samudra Uduwaka \ +_Created_: 2025-01-02 \ +_Updated_: 2025-01-07 \ _Edition_: Swan Lake # Sanitation for OpenAPI specification This document records the sanitation done on top of the official OpenAPI specification from HubSpot Marketing Campaigns . -The OpenAPI specification is obtained from (TODO: Add source link). -These changes are done in order to improve the overall usability, and as workarounds for some known language limitations. +The OpenAPI specification is obtained from [Hubspot Marketing Campaings API](https://developers.hubspot.com/docs/reference/api/marketing/campaigns). + + +1. Change the url property of the servers object: + + Original: `https://api.hubapi.com` + + Updated: `https://api.hubapi.com/marketing/v3/campaigns` + + Reason: This change is made to ensure that all API paths are relative to the versioned base URL (/marketing/v3/campaigns), which improves the consistency and usability of the APIs. + +2. Update API Paths: + + Original: Paths included the version prefix in each endpoint (e.g., /marketing/v3/campaigns/batch/read ). + + Updated: Paths are modified to remove the version prefix from the endpoints, as it is now included in the base URL. For example: + + - Original: `/marketing/v3/campaigns/batch/read` + + - Updated: `/batch/read` + + Reason: This modification simplifies the API paths, making them shorter and more readable. It also centralizes the versioning to the base URL, which is a common best practice. + +3. Updated `PublicCampaignAsset` Object: + + Removed `metrics` property from the required properties. + + Definitions: + + - Previous Definition: + + ```json + "PublicCampaignAsset" : { + "required" : [ "id", "metrics"], + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + }, + "id" : { + "type" : "string" + }, + "metrics" : { + "type" : "object", + "additionalProperties" : { + "type" : "number" + } + } + } + } + ``` + + - After + + ```json + "PublicCampaignAsset" : { + "required" : [ "id"], + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + }, + "id" : { + "type" : "string" + }, + "metrics" : { + "type" : "object", + "additionalProperties" : { + "type" : "number" + } + } + } + } + ``` + + Reason: `metrics` property is not required when creating and retrieving. -[//]: # (TODO: Add sanitation details) -1. -2. -3. ## OpenAPI cli command The following command was used to generate the Ballerina client from the OpenAPI specification. The command should be executed from the repository root directory. ```bash -# TODO: Add OpenAPI CLI command used to generate the client +bal openapi -i docs/spec/openapi.json --mode client --license docs/license.txt -o ballerina ``` Note: The license year is hardcoded to 2024, change if necessary. diff --git a/examples/README.md b/examples/README.md index 1135ccd..f88730c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,14 +2,23 @@ The `ballerinax/hubspot.marketing.campaigns` connector provides practical examples illustrating usage in various scenarios. -[//]: # (TODO: Add examples) -1. -2. +1. [Batch of Campaigns](./batch_of_campaigns/) - Fully manage a batch of campaigns +2. [Campaign Lifecycle with Assets](./campaign_lifecycle_with_assets/) - Full life cycle of a campaign associated with assets such as forms + ## Prerequisites -[//]: # (TODO: Add prerequisites) +1. Setup Hubspot account +Refer to the Setup guide in [README.md](README.md) file to set up your hubspot account, if you do not have one. + +2. Configuration +Update your Zendesk account related configurations in the Config.toml file in the example root directory: +```toml + clientId = "" + clientSecret = "" + refreshToken = "" +``` ## Running an example Execute the following commands to build an example from the source: diff --git a/examples/batch_of_campaigns/Ballerina.toml b/examples/batch_of_campaigns/Ballerina.toml new file mode 100644 index 0000000..434c1c1 --- /dev/null +++ b/examples/batch_of_campaigns/Ballerina.toml @@ -0,0 +1,8 @@ +[package] +org = "wso2" +name = "batch_of_campaigns" +version = "0.1.0" +distribution = "2201.10.3" + +[build-options] +observabilityIncluded = true \ No newline at end of file diff --git a/examples/batch_of_campaigns/main.bal b/examples/batch_of_campaigns/main.bal new file mode 100644 index 0000000..e7c2abf --- /dev/null +++ b/examples/batch_of_campaigns/main.bal @@ -0,0 +1,102 @@ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; +import ballerina/io; +import ballerina/oauth2; +import ballerina/time; +import ballerinax/hubspot.marketing.campaigns; + +configurable string clientId = ?; +configurable string clientSecret = ?; +configurable string refreshToken = ?; + +campaigns:OAuth2RefreshTokenGrantConfig auth = { + clientId, + clientSecret, + refreshToken, + credentialBearer: oauth2:POST_BODY_BEARER +}; +final campaigns:Client hubspotMarketingCampaign = check new ({auth}); + +public function main() returns error? { + + campaigns:BatchInputPublicCampaignInput inputBatchCampaignsDefinition = { + "inputs": [ + { + "properties": { + "hs_name": "batchCampaign1_" + time:utcToString(time:utcNow()), + "hs_goal": "goalForBatchCampaign1" + } + }, + { + "properties": { + "hs_name": "batchCampaign2_" + time:utcToString(time:utcNow()), + "hs_goal": "goalForBatchCampaign2" + } + } + ] + }; + + campaigns:BatchResponsePublicCampaign response = check hubspotMarketingCampaign->/batch/create.post( + inputBatchCampaignsDefinition + ); + string campaignId1 = response?.results[0]?.id; + string campaignId2 = response?.results[1]?.id; + + if campaignId1 == "" || campaignId2 == "" { + io:println("Batch creation is not successful"); + } else { + io:println("Batch creation is successful"); + } + + campaigns:BatchResponsePublicCampaignWithAssets|campaigns:BatchResponsePublicCampaignWithAssetsWithErrors + batchReadResponse = check hubspotMarketingCampaign->/batch/read.post( + payload = { + "inputs": [ + {"id": campaignId1}, + {"id": campaignId2} + ] + } + ); + + if batchReadResponse?.status == "COMPLETE" { + io:println("Batch read is successful"); + } else { + io:println("Batch read is not successful"); + } + + http:Response batchDeleteResponse = check hubspotMarketingCampaign->/batch/archive.post( + payload = { + "inputs": [ + { + "id": campaignId1 + }, + { + "id": campaignId2 + } + ] + } + ); + + int deleteResponse = batchDeleteResponse.statusCode; + + if deleteResponse == 204 { + io:println("Batch deletion is successful"); + } else { + io:println("Batch deletion is not successful"); + } +}; diff --git a/examples/campaign_lifecycle_with_assets/Ballerina.toml b/examples/campaign_lifecycle_with_assets/Ballerina.toml new file mode 100644 index 0000000..9061f43 --- /dev/null +++ b/examples/campaign_lifecycle_with_assets/Ballerina.toml @@ -0,0 +1,8 @@ +[package] +org = "wso2" +name = "campaign_lifecycle_with_assets" +version = "0.1.0" +distribution = "2201.10.3" + +[build-options] +observabilityIncluded = true diff --git a/examples/campaign_lifecycle_with_assets/main.bal b/examples/campaign_lifecycle_with_assets/main.bal new file mode 100644 index 0000000..45aa12f --- /dev/null +++ b/examples/campaign_lifecycle_with_assets/main.bal @@ -0,0 +1,95 @@ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; +import ballerina/io; +import ballerina/oauth2; +import ballerina/time; +import ballerinax/hubspot.marketing.campaigns; + +configurable string clientId = ?; +configurable string clientSecret = ?; +configurable string refreshToken = ?; + +campaigns:OAuth2RefreshTokenGrantConfig auth = { + clientId, + clientSecret, + refreshToken, + credentialBearer: oauth2:POST_BODY_BEARER +}; + +final campaigns:Client hubspotMarketingCampaigns = check new ({auth}); + +configurable string assetType = ?; +configurable string assetID = ?; + +public function main() returns error? { + + campaigns:PublicCampaignInput inputCampaignDefinition = { + properties: { + "hs_name": "campaign" + time:utcNow().toString(), + "hs_goal": "campaignGoalSpecified", + "hs_notes": "someNotesForTheCampaign" + } + }; + + campaigns:PublicCampaign response = check hubspotMarketingCampaigns->/.post( + inputCampaignDefinition + ); + + string campaignId = response?.id; + + if campaignId == "" { + io:println("Campaign creation is not successful"); + } else { + io:println(string `Campaign with id "${campaignId}" created successfully`); + } + + var addAssetResponse = check hubspotMarketingCampaigns->/[campaignId]/assets/[assetType]/[assetID].put(); + + if addAssetResponse.statusCode == 204 { + io:println("Asset association is successful"); + } else { + io:println("Asset association is not successful"); + } + + campaigns:CollectionResponsePublicCampaignAssetForwardPaging assetListResponse = + check hubspotMarketingCampaigns->/[campaignId]/assets/[assetType]; + + if assetListResponse.results.length() > 0 { + io:println("Asset list retrieval is successful"); + io:println("Asset ID: " + assetListResponse.results[0].id); + } else { + io:println("Asset list retrieval is not successful"); + } + + http:Response assetRemoveResponse = check hubspotMarketingCampaigns->/[campaignId]/assets/[assetType]/[assetID].delete(); + + if assetRemoveResponse.statusCode == 204 { + io:println("Asset removal is successful"); + } else { + io:println("Asset removal is not successful"); + } + + http:Response deleteCampaignResponse = check hubspotMarketingCampaigns->/[campaignId].delete(); + + if deleteCampaignResponse.statusCode == 204 { + io:println("Deletion is successful"); + } else { + io:println("Deletion is not successful"); + } + +};