├── .gitattributes ├── MIGRATION.md ├── README.md ├── composer.json └── src ├── ConvertKit_API.php └── ConvertKit_API_Traits.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /.github/ export-ignore 2 | /docs/ export-ignore 3 | /tests/ export-ignore 4 | .env.dist.testing export-ignore 5 | .env.example export-ignore 6 | .gitignore export-ignore 7 | /CONTRIBUTING.md export-ignore 8 | /DEPLOYMENT.md export-ignore 9 | /DEVELOPMENT.md export-ignore 10 | /phpcs.tests.xml export-ignore 11 | /phpcs.xml export-ignore 12 | /phpstan.neon.dist export-ignore 13 | /phpstan.neon.example export-ignore 14 | /phpunit.xml export-ignore 15 | /SETUP.md export-ignore 16 | /TESTING.md export-ignore -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | # Migrating from v1.x SDK (v3 API) to v2.x SDK (v4 API) 2 | 3 | Whilst every best effort is made to minimise the number of breaking changes, some breaking changes exist to ensure improved method naming conventions and compatibility with OAuth authentication and the v4 API. 4 | 5 | This guide is designed to cover changes that developers may need to make to their existing implementation when upgrading to the v2 SDK. 6 | 7 | ## PHP Version 8 | 9 | The minimum supported PHP version is `8.0`. Users on older PHP versions should continue to use the v1 SDK. 10 | 11 | ## Authentication 12 | 13 | Authentication is now via OAuth. It's recommended to refer to the README file's [`Getting Started`](README.md#2x-v4-api-oauth-php-80) section for implementation. 14 | 15 | Initializing the `ConvertKit_API` class now accepts a `clientID`, `clientSecret` and `accessToken` in place of the existing `api_key` and `api_secret`: 16 | 17 | ```php 18 | $api = new \ConvertKit_API\ConvertKit_API( 19 | clientID: '', 20 | clientSecret: '', 21 | accessToken: '' 22 | ); 23 | ``` 24 | 25 | ## Pagination 26 | 27 | For list based endpoints which fetch data from the API (such as broadcasts, custom fields, subscribers, tags, email templates, forms, purchases etc.), cursor based pagination is used. The following parameters can be specified in the API methods: 28 | 29 | - `per_page`: Defines the number of results to return, with a maximum value of 100 30 | - `after_cursor`: When specified, returns the next page of results based on the current result's `pagination->end_cursor` value 31 | - `before_cursor`: When specified, returns the previous page of results based on the current result's `pagination->start_cursor` value 32 | 33 | ## Accounts 34 | 35 | - Added: `get_account_colors()` 36 | - Added: `update_account_colors()` 37 | - Added: `get_creator_profile()` 38 | - Added: `get_email_stats()` 39 | - Added: `get_growth_stats()` 40 | 41 | ## Broadcasts 42 | 43 | - Updated: `get_broadcasts()` supports pagination 44 | - Updated: `create_broadcast()`: 45 | - `email_layout_template` is now `email_template_id`. To fetch the ID of the account's email templates, refer to `get_email_templates()` 46 | - `preview_text` option added 47 | - `subscriber_filter` option added 48 | - Updated: `update_broadcast()` 49 | - `email_layout_template` is now `email_template_id`. To fetch the ID of the account's email templates, refer to `get_email_templates()` 50 | - `preview_text` option added 51 | - `subscriber_filter` option added 52 | - Changed: `destroy_broadcast()` is renamed to `delete_broadcast()` 53 | 54 | ## Custom Fields 55 | 56 | - Added: `create_custom_fields()` to create multiple custom fields in a single request 57 | - Updated: `get_custom_fields()` supports pagination 58 | 59 | ## Subscribers 60 | 61 | - Added: `create_subscriber()`. The concept of creating a subscriber via a form, tag or sequence is replaced with this new method. The subscriber can then be subscribed to resources (forms, tag, sequences) as necessary. 62 | - Added: `create_subscribers()` to create multiple subscribers in a single request 63 | - Added: `get_subscribers()` 64 | - Changed: `unsubscribe()` is now `unsubscribe_by_email()`. Use `unsubscribe()` for unsubscribing by a subscriber ID 65 | - Updated: `get_subscriber_tags()` supports pagination 66 | 67 | ## Tags 68 | 69 | - Added: `create_tags()` to create multiple tags in a single request 70 | - Updated: `get_tags()` supports pagination 71 | - Updated: `get_tag_subscriptions()`: 72 | - supports pagination 73 | - supports filtering by subscribers by dates, covering `created_after`, `created_before`, `tagged_after` and `tagged_before` 74 | - `sort_order` is no longer supported 75 | - Changed: `tag_subscriber()` is now `tag_subscriber_by_email()`. Use `tag_subscriber()` for tagging by subscriber ID 76 | 77 | ## Email Templates 78 | 79 | - Added: `get_email_templates()` 80 | 81 | ## Forms 82 | 83 | - Updated: `get_forms()`: 84 | - supports pagination 85 | - only returns active forms by default. Use the `status` parameter to filter by `active`, `archived`, `trashed` or `all` 86 | - Updated: `get_landing_pages()`: 87 | - supports pagination 88 | - only returns active landing pages by default. Use the `status` parameter to filter by `active`, `archived`, `trashed` or `all` 89 | - Updated: `get_form_subscriptions()`: 90 | - supports pagination 91 | - supports filtering by subscribers by dates, covering `created_after`, `created_before`, `added_after` and `added_before` 92 | - `sort_order` is no longer supported 93 | - Changed: `add_subscriber_to_form()` is now `add_subscriber_to_form_by_email()`. Use `add_subscriber_to_form()` for adding subscriber to form by subscriber ID 94 | 95 | ## Purchases 96 | 97 | - Updated: `create_purchase()` now supports named parameters for purchase data, instead of an `$options` array 98 | - Changed: `list_purchases()` is now `get_purchases()`, with pagination support 99 | 100 | ## Segments 101 | 102 | - Added: `get_segments()` 103 | 104 | ## Sequences 105 | 106 | - Changed: `add_subscriber_to_sequence()` is now `add_subscriber_to_sequence_by_email()`. Use `add_subscriber_to_sequence()` for adding a subscriber to a sequence by subscriber ID 107 | - Updated: `get_sequences()` supports pagination 108 | - Updated: `get_sequence_subscriptions()`: 109 | - supports pagination 110 | - supports filtering by subscribers by dates, covering `created_after`, `created_before`, `added_after` and `added_before` 111 | - `sort_order` is no longer supported 112 | 113 | ## Webhooks 114 | 115 | - Added: `get_webhooks()` 116 | - Changed: `destroy_webhook()` is now `delete_webhook()` 117 | 118 | ## Other 119 | 120 | - Removed: `form_subscribe()` was previously deprecated. Use `add_subscriber_to_form()` or `add_subscriber_to_form_by_email()` 121 | - Removed: `add_tag()` was previously deprecated. Use `tag_subscriber()` or `tag_subscriber_by_email()` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kit SDK PHP 2 | 3 | The Kit PHP SDK provides convinient access to the Kit API from applications written in the PHP language. 4 | 5 | It includes a pre-defined set of methods for interacting with the API. 6 | 7 | ## Version Guidance 8 | 9 | | SDK Version | API Version | API Authentication | PHP Version | 10 | |-------------|-------------|--------------------|--------------| 11 | | 1.x | v3 | API Key and Secret | 7.4+ | 12 | | 2.x | v4 | OAuth | 8.0+ | 13 | | 2.2+ | v4 | API Key | 8.0+ | 14 | 15 | Refer to [this guide](MIGRATION.md) for changes when upgrading to the v2 SDK. 16 | 17 | ## Composer 18 | 19 | You can install this PHP SDK via [Composer](http://getcomposer.org/). Run the following command: 20 | 21 | ```bash 22 | composer require convertkit/convertkitapi 23 | ``` 24 | 25 | To use the PHP SDK, use Composer's [autoload](https://getcomposer.org/doc/01-basic-usage.md#autoloading): 26 | 27 | ```php 28 | require_once 'vendor/autoload.php'; 29 | ``` 30 | 31 | ## Dependencies 32 | 33 | The PHP SDK require the following extensions in order to work properly: 34 | 35 | - [`curl`](https://secure.php.net/manual/en/book.curl.php), although you can use your own non-cURL client if you prefer 36 | - [`json`](https://secure.php.net/manual/en/book.json.php) 37 | - [`mbstring`](https://secure.php.net/manual/en/book.mbstring.php) (Multibyte String) 38 | 39 | If you use Composer, these dependencies should be handled automatically. 40 | 41 | ## Getting Started 42 | 43 | ### 2.x (v4 API, OAuth, PHP 8.0+) 44 | 45 | First, register your OAuth application in the `OAuth Applications` section at https://app.kit.com/account_settings/advanced_settings. 46 | 47 | Using the supplied Client ID and secret, redirect the user to Kit to grant your application access to their Kit account. 48 | 49 | ```php 50 | // Require the autoloader (if you're using a PHP framework, this may already be done for you). 51 | require_once 'vendor/autoload.php'; 52 | 53 | // Initialize the API class. 54 | $api = new \ConvertKit_API\ConvertKit_API( 55 | clientID: '', 56 | clientSecret: '' 57 | ); 58 | 59 | // Redirect to begin the OAuth process. 60 | header('Location: '.$api->get_oauth_url('')); 61 | ``` 62 | 63 | Once the user grants your application access to their Kit account, they'll be redirected to your Redirect URI with an authorization code. For example: 64 | 65 | `your-redirect-uri?code=` 66 | 67 | At this point, your application needs to exchange the authorization code for an access token and refresh token. 68 | 69 | ```php 70 | $result = $api->get_access_token( 71 | authCode: '', 72 | redirectURI: '' 73 | ); 74 | ``` 75 | 76 | `$result` is an array comprising of: 77 | - `access_token`: The access token, used to make authenticated requests to the API 78 | - `refresh_token`: The refresh token, used to fetch a new access token once the current access token has expired 79 | - `created_at`: When the access token was created 80 | - `expires_in`: The number of seconds from `created_at` that the access token will expire 81 | 82 | Once you have an access token, re-initialize the API class with it: 83 | 84 | ```php 85 | // Initialize the API class. 86 | $api = new \ConvertKit_API\ConvertKit_API( 87 | clientID: '', 88 | clientSecret: '', 89 | accessToken: '' 90 | ); 91 | ``` 92 | 93 | To refresh an access token: 94 | 95 | ```php 96 | $result = $api->refresh_token( 97 | refreshToken: '', 98 | redirectURI: '' 99 | ); 100 | ``` 101 | 102 | `$result` is an array comprising of: 103 | - `access_token`: The access token, used to make authenticated requests to the API 104 | - `refresh_token`: The refresh token, used to fetch a new access token once the current access token has expired 105 | - `created_at`: When the access token was created 106 | - `expires_in`: The number of seconds from `created_at` that the access token will expire 107 | 108 | Once you have refreshed the access token i.e. obtained a new access token, re-initialize the API class with it: 109 | 110 | ```php 111 | // Initialize the API class. 112 | $api = new \ConvertKit_API\ConvertKit_API( 113 | clientID: '', 114 | clientSecret: '', 115 | accessToken: '' 116 | ); 117 | ``` 118 | 119 | API requests may then be performed: 120 | 121 | ```php 122 | $result = $api->add_subscriber_to_form(12345, 'joe.bloggs@kit.com'); 123 | ``` 124 | 125 | To determine whether a new entity / relationship was created, or an existing entity / relationship updated, inspect the HTTP code of the last request: 126 | 127 | ```php 128 | $result = $api->add_subscriber_to_form(12345, 'joe.bloggs@kit.com'); 129 | $code = $api->getResponseInterface()->getStatusCode(); // 200 OK if e.g. a subscriber already added to the specified form, 201 Created if the subscriber added to the specified form for the first time. 130 | ``` 131 | 132 | The PSR-7 response can be fetched and further inspected, if required - for example, to check if a header exists: 133 | 134 | ```php 135 | $result = $api->add_subscriber_to_form(12345, 'joe.bloggs@kit.com'); 136 | $api->getResponseInterface()->hasHeader('Content-Length'); // Check if the last API request included a `Content-Length` header 137 | ``` 138 | 139 | ### 2.2+ (v4 API, API Key, PHP 8.0+) 140 | 141 | Get your Kit API Key and API Secret [here](https://app.kit.com/account_settings/developer_settings) and set it somewhere in your application. 142 | 143 | ```php 144 | // Require the autoloader (if you're using a PHP framework, this may already be done for you). 145 | require_once 'vendor/autoload.php'; 146 | 147 | // Initialize the API class. 148 | $api = new \ConvertKit_API\ConvertKit_API( 149 | apiKey: '' 150 | ); 151 | ``` 152 | 153 | ### 1.x (v3 API, API Key and Secret, PHP 7.4+) 154 | 155 | Get your Kit API Key and API Secret [here](https://app.kit.com/account_settings/developer_settings) and set it somewhere in your application. 156 | 157 | ```php 158 | // Require the autoloader (if you're using a PHP framework, this may already be done for you). 159 | require_once 'vendor/autoload.php'; 160 | 161 | // Initialize the API class. 162 | $api = new \ConvertKit_API\ConvertKit_API('', ''); 163 | ``` 164 | 165 | ## Handling Errors 166 | 167 | The Kit PHP SDK uses Guzzle for all HTTP API requests. Errors will be thrown as Guzzle's `ClientException` (for 4xx errors), 168 | or `ServerException` (for 5xx errors). 169 | 170 | ```php 171 | try { 172 | $forms = $api->add_subscriber_to_form('invalid-form-id'); 173 | } catch (GuzzleHttp\Exception\ClientException $e) { 174 | // Handle 4xx client errors. 175 | die($e->getMessage()); 176 | } catch (GuzzleHttp\Exception\ServerException $e) { 177 | // Handle 5xx server errors. 178 | die($e->getMessage()); 179 | } 180 | ``` 181 | 182 | For a more detailed error message, it's possible to fetch the API's response when a `ClientException` is thrown: 183 | 184 | ```php 185 | // Errors will be thrown as Guzzle's ClientException or ServerException. 186 | try { 187 | $forms = $api->form_subscribe('invalid-form-id'); 188 | } catch (GuzzleHttp\Exception\ClientException $e) { 189 | // Handle 4xx client errors. 190 | // For ClientException, it's possible to inspect the API's JSON response 191 | // to output an error or handle it accordingly. 192 | $error = json_decode($e->getResponse()->getBody()->getContents()); 193 | die($error->message); // e.g. "Entity not found". 194 | } catch (GuzzleHttp\Exception\ServerException $e) { 195 | // Handle 5xx server errors. 196 | die($e->getMessage()); 197 | } 198 | ``` 199 | 200 | ## Documentation 201 | 202 | See the [PHP SDK docs](./docs/classes/ConvertKit_API/ConvertKit_API.md) 203 | 204 | ## Contributing 205 | 206 | See our [contributor guide](CONTRIBUTING.md) for setting up your development environment, testing and submitting a PR. 207 | 208 | For Kit, refer to the [deployment guide](DEPLOYMENT.md) on how to publish a new release. 209 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convertkit/convertkitapi", 3 | "description": "ConvertKit PHP SDK for the ConvertKit API", 4 | "keywords": [ 5 | "convertkit", 6 | "api", 7 | "forms" 8 | ], 9 | "homepage": "https://convertkit.com/", 10 | "license": "GPLv3", 11 | "authors": [ 12 | { 13 | "name": "ConvertKit and contributors", 14 | "homepage": "https://github.com/convertkit/convertkitsdk-php/contributors" 15 | } 16 | ], 17 | "require": { 18 | "php": ">=8.0", 19 | "guzzlehttp/guzzle": "^7.0", 20 | "monolog/monolog": "^2.5|^3.0" 21 | }, 22 | "require-dev": { 23 | "vlucas/phpdotenv": "^5.5", 24 | "phpunit/phpunit": "^5.7 || ^9.0", 25 | "squizlabs/php_codesniffer": "^3.3", 26 | "phpstan/phpstan": "^2.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "ConvertKit_API\\": "src/" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "": "tests/" 36 | } 37 | }, 38 | "minimum-stability": "dev", 39 | "prefer-stable": true, 40 | "scripts": { 41 | "create-docs": "phpDocumentor --directory=src --target=docs --template='vendor/saggre/phpdocumentor-markdown/themes/markdown'" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ConvertKit_API.php: -------------------------------------------------------------------------------- 1 | client_id = $clientID; 78 | $this->client_secret = $clientSecret; 79 | $this->access_token = $accessToken; 80 | $this->api_key = $apiKey; 81 | $this->debug = $debug; 82 | 83 | // Set the Guzzle client. 84 | $this->client = new Client(); 85 | 86 | if ($debug) { 87 | // If no debug log file location specified, define a default. 88 | if (empty($debugLogFileLocation)) { 89 | $debugLogFileLocation = __DIR__ . '/logs/debug.log'; 90 | } 91 | 92 | $this->debug_logger = new Logger('ck-debug'); 93 | $stream_handler = new StreamHandler($debugLogFileLocation, Logger::DEBUG); 94 | $this->debug_logger->pushHandler( 95 | $stream_handler // phpcs:ignore Squiz.Objects.ObjectInstantiation.NotAssigned 96 | ); 97 | } 98 | } 99 | 100 | /** 101 | * Set the Guzzle client implementation to use for API requests. 102 | * 103 | * @param ClientInterface $client Guzzle client implementation. 104 | * 105 | * @since 1.3.0 106 | * 107 | * @return void 108 | */ 109 | public function set_http_client(ClientInterface $client) 110 | { 111 | $this->client = $client; 112 | } 113 | 114 | /** 115 | * Add an entry to monologger. 116 | * 117 | * @param string $message Message. 118 | * 119 | * @return void 120 | */ 121 | private function create_log(string $message) 122 | { 123 | // Don't log anything if debugging isn't enabled. 124 | if (!$this->debug) { 125 | return; 126 | } 127 | 128 | // Mask the Client ID, Client Secret, Access Token, and API Key. 129 | if ($this->client_id) { 130 | $message = str_replace( 131 | $this->client_id, 132 | str_repeat('*', (strlen($this->client_id) - 4)) . substr($this->client_id, - 4), 133 | $message 134 | ); 135 | } 136 | if ($this->client_secret) { 137 | $message = str_replace( 138 | $this->client_secret, 139 | str_repeat('*', (strlen($this->client_secret) - 4)) . substr($this->client_secret, - 4), 140 | $message 141 | ); 142 | } 143 | if ($this->access_token) { 144 | $message = str_replace( 145 | $this->access_token, 146 | str_repeat('*', (strlen($this->access_token) - 4)) . substr($this->access_token, - 4), 147 | $message 148 | ); 149 | } 150 | if ($this->api_key) { 151 | $message = str_replace( 152 | $this->api_key, 153 | str_repeat('*', (strlen($this->api_key) - 4)) . substr($this->api_key, - 4), 154 | $message 155 | ); 156 | } 157 | 158 | // Mask email addresses that may be contained within the message. 159 | $message = preg_replace_callback( 160 | '^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})^', 161 | // @phpstan-ignore-next-line - see https://github.com/phpstan/phpstan/issues/10396 162 | function ($matches) { 163 | return preg_replace('/\B[^@.]/', '*', $matches[0]); 164 | }, 165 | $message 166 | ); 167 | 168 | // Add to log. 169 | $this->debug_logger->info((string) $message); 170 | } 171 | 172 | /** 173 | * Returns the OAuth URL to begin the OAuth process. 174 | * 175 | * @param string $redirectURI Redirect URI. 176 | * 177 | * @return string 178 | */ 179 | public function get_oauth_url(string $redirectURI) 180 | { 181 | return $this->oauth_authorize_url . '?' . http_build_query( 182 | [ 183 | 'client_id' => $this->client_id, 184 | 'redirect_uri' => $redirectURI, 185 | 'response_type' => 'code', 186 | ] 187 | ); 188 | } 189 | 190 | /** 191 | * Exchanges the given authorization code for an access token and refresh token. 192 | * 193 | * @param string $authCode Authorization Code, returned from get_oauth_url() flow. 194 | * @param string $redirectURI Redirect URI. 195 | * 196 | * @return mixed|array API response 197 | */ 198 | public function get_access_token(string $authCode, string $redirectURI) 199 | { 200 | // Build request. 201 | $request = new Request( 202 | method: 'POST', 203 | uri: $this->oauth_token_url, 204 | headers: $this->get_request_headers( 205 | auth: false 206 | ), 207 | body: (string) json_encode( 208 | [ 209 | 'code' => $authCode, 210 | 'client_id' => $this->client_id, 211 | 'client_secret' => $this->client_secret, 212 | 'grant_type' => 'authorization_code', 213 | 'redirect_uri' => $redirectURI, 214 | ] 215 | ) 216 | ); 217 | 218 | // Send request. 219 | $response = $this->client->send( 220 | $request, 221 | ['exceptions' => false] 222 | ); 223 | 224 | // Return response body. 225 | return json_decode($response->getBody()->getContents()); 226 | } 227 | 228 | /** 229 | * Fetches a new access token using the supplied refresh token. 230 | * 231 | * @param string $refreshToken Refresh Token. 232 | * @param string $redirectURI Redirect URI. 233 | * 234 | * @return mixed|array API response 235 | */ 236 | public function refresh_token(string $refreshToken, string $redirectURI) 237 | { 238 | // Build request. 239 | $request = new Request( 240 | method: 'POST', 241 | uri: $this->oauth_token_url, 242 | headers: $this->get_request_headers( 243 | auth: false 244 | ), 245 | body: (string) json_encode( 246 | [ 247 | 'refresh_token' => $refreshToken, 248 | 'client_id' => $this->client_id, 249 | 'client_secret' => $this->client_secret, 250 | 'grant_type' => 'refresh_token', 251 | 'redirect_uri' => $redirectURI, 252 | ] 253 | ) 254 | ); 255 | 256 | // Send request. 257 | $response = $this->client->send( 258 | $request, 259 | ['exceptions' => false] 260 | ); 261 | 262 | // Return response body. 263 | return json_decode($response->getBody()->getContents()); 264 | } 265 | 266 | /** 267 | * Get markup from ConvertKit for the provided $url. 268 | * 269 | * Supports legacy forms and legacy landing pages. 270 | * 271 | * Forms and Landing Pages should be embedded using the supplied JS embed script in 272 | * the API response when using get_forms() or get_landing_pages(). 273 | * 274 | * @param string $url URL of HTML page. 275 | * 276 | * @throws \InvalidArgumentException If the URL is not a valid URL format. 277 | * @throws \Exception If parsing the legacy form or landing page failed. 278 | * 279 | * @return false|string 280 | */ 281 | public function get_resource(string $url) 282 | { 283 | if (!filter_var($url, FILTER_VALIDATE_URL)) { 284 | throw new \InvalidArgumentException(); 285 | } 286 | 287 | $resource = ''; 288 | 289 | $this->create_log(sprintf('Getting resource %s', $url)); 290 | 291 | // Fetch the resource. 292 | $request = new Request( 293 | method: 'GET', 294 | uri: $url, 295 | headers: $this->get_request_headers( 296 | type: 'text/html', 297 | auth: false 298 | ), 299 | ); 300 | $response = $this->client->send($request); 301 | 302 | // Fetch HTML. 303 | $body = $response->getBody()->getContents(); 304 | 305 | // Forcibly tell DOMDocument that this HTML uses the UTF-8 charset. 306 | // isn't enough, as DOMDocument still interprets the HTML as ISO-8859, 307 | // which breaks character encoding. 308 | // Use of mb_convert_encoding() with HTML-ENTITIES is deprecated in PHP 8.2, so we have to use this method. 309 | // If we don't, special characters render incorrectly. 310 | $body = str_replace( 311 | '', 312 | '' . "\n" . '', 313 | $body 314 | ); 315 | 316 | // Get just the scheme and host from the URL. 317 | $url_scheme_host_only = parse_url($url, PHP_URL_SCHEME) . '://' . parse_url($url, PHP_URL_HOST); 318 | 319 | // Load the HTML into a DOMDocument. 320 | libxml_use_internal_errors(true); 321 | $html = new \DOMDocument(); 322 | $html->loadHTML($body); 323 | 324 | // Convert any relative URLs to absolute URLs in the HTML DOM. 325 | $this->convert_relative_to_absolute_urls($html->getElementsByTagName('a'), 'href', $url_scheme_host_only); 326 | $this->convert_relative_to_absolute_urls($html->getElementsByTagName('link'), 'href', $url_scheme_host_only); 327 | $this->convert_relative_to_absolute_urls($html->getElementsByTagName('img'), 'src', $url_scheme_host_only); 328 | $this->convert_relative_to_absolute_urls($html->getElementsByTagName('script'), 'src', $url_scheme_host_only); 329 | $this->convert_relative_to_absolute_urls($html->getElementsByTagName('form'), 'action', $url_scheme_host_only); 330 | 331 | // Save HTML. 332 | $resource = $html->saveHTML(); 333 | 334 | // If the result is false, return a blank string. 335 | if (!$resource) { 336 | throw new \Exception(sprintf('Could not parse %s', $url)); 337 | } 338 | 339 | // Remove some HTML tags that DOMDocument adds, returning the output. 340 | // We do this instead of using LIBXML_HTML_NOIMPLIED in loadHTML(), because Legacy Forms 341 | // are not always contained in a single root / outer element, which is required for 342 | // LIBXML_HTML_NOIMPLIED to correctly work. 343 | $resource = $this->strip_html_head_body_tags($resource); 344 | 345 | return $resource; 346 | } 347 | 348 | /** 349 | * Performs an API request using Guzzle. 350 | * 351 | * @param string $endpoint API Endpoint. 352 | * @param string $method Request method. 353 | * @param array>> $args Request arguments. 354 | * 355 | * @throws \Exception If JSON encoding arguments failed. 356 | * 357 | * @return mixed|object 358 | */ 359 | public function request(string $endpoint, string $method, array $args = []) 360 | { 361 | // Build URL. 362 | $url = $this->api_url_base . $this->api_version . '/' . $endpoint; 363 | 364 | // Log request. 365 | $this->create_log(sprintf('%s %s', $method, $endpoint)); 366 | $this->create_log(sprintf('%s', json_encode($args))); 367 | 368 | // Build request. 369 | switch ($method) { 370 | case 'GET': 371 | if ($args) { 372 | $url .= '?' . http_build_query($args); 373 | } 374 | 375 | $request = new Request( 376 | method: $method, 377 | uri: $url, 378 | headers: $this->get_request_headers(), 379 | ); 380 | break; 381 | 382 | default: 383 | $request = new Request( 384 | method: $method, 385 | uri: $url, 386 | headers: $this->get_request_headers(), 387 | body: (string) json_encode($args), 388 | ); 389 | break; 390 | }//end switch 391 | 392 | // Send request. 393 | $this->response = $this->client->send( 394 | $request, 395 | ['exceptions' => false] 396 | ); 397 | 398 | // Get response. 399 | $response_body = $this->response->getBody()->getContents(); 400 | 401 | // Log response. 402 | $this->create_log(sprintf('Response Status Code: %s', $this->response->getStatusCode())); 403 | $this->create_log(sprintf('Response Body: %s', $response_body)); 404 | $this->create_log('Finish request successfully'); 405 | 406 | // Return response. 407 | return json_decode($response_body); 408 | } 409 | 410 | /** 411 | * Returns the response interface used for the last API request. 412 | * 413 | * @since 2.0.0 414 | * 415 | * @return \Psr\Http\Message\ResponseInterface 416 | */ 417 | public function getResponseInterface() 418 | { 419 | return $this->response; 420 | } 421 | 422 | /** 423 | * Returns the headers to use in an API request. 424 | * 425 | * @param string $type Accept and Content-Type Headers. 426 | * @param boolean $auth Include authorization header. 427 | * 428 | * @since 2.0.0 429 | * 430 | * @return array 431 | */ 432 | public function get_request_headers(string $type = 'application/json', bool $auth = true) 433 | { 434 | $headers = [ 435 | 'Accept' => $type, 436 | 'Content-Type' => $type . '; charset=utf-8', 437 | 'User-Agent' => $this->get_user_agent(), 438 | ]; 439 | 440 | // If no authorization header required, return now. 441 | if (!$auth) { 442 | return $headers; 443 | } 444 | 445 | // Add authorization header and return. 446 | if ($this->api_key) { 447 | $headers['X-Kit-Api-Key'] = $this->api_key; 448 | } else if ($this->access_token) { 449 | $headers['Authorization'] = 'Bearer ' . $this->access_token; 450 | } 451 | 452 | return $headers; 453 | } 454 | 455 | /** 456 | * Returns the maximum amount of time to wait for 457 | * a response to the request before exiting. 458 | * 459 | * @since 2.0.0 460 | * 461 | * @return integer Timeout, in seconds. 462 | */ 463 | public function get_timeout() 464 | { 465 | $timeout = 10; 466 | 467 | return $timeout; 468 | } 469 | 470 | /** 471 | * Returns the user agent string to use in all HTTP requests. 472 | * 473 | * @since 2.0.0 474 | * 475 | * @return string 476 | */ 477 | public function get_user_agent() 478 | { 479 | return 'ConvertKitPHPSDK/' . self::VERSION . ';PHP/' . phpversion(); 480 | } 481 | } 482 | -------------------------------------------------------------------------------- /src/ConvertKit_API_Traits.php: -------------------------------------------------------------------------------- 1 | get('account'); 82 | } 83 | 84 | /** 85 | * Gets the account's colors 86 | * 87 | * @see https://developers.convertkit.com/v4.html#list-colors 88 | * 89 | * @return false|mixed 90 | */ 91 | public function get_account_colors() 92 | { 93 | return $this->get('account/colors'); 94 | } 95 | 96 | /** 97 | * Gets the account's colors 98 | * 99 | * @param array $colors Hex colors. 100 | * 101 | * @see https://developers.convertkit.com/v4.html#list-colors 102 | * 103 | * @return false|mixed 104 | */ 105 | public function update_account_colors(array $colors) 106 | { 107 | return $this->put( 108 | 'account/colors', 109 | ['colors' => $colors] 110 | ); 111 | } 112 | 113 | /** 114 | * Gets the Creator Profile 115 | * 116 | * @see https://developers.convertkit.com/v4.html#get-creator-profile 117 | * 118 | * @return false|mixed 119 | */ 120 | public function get_creator_profile() 121 | { 122 | return $this->get('account/creator_profile'); 123 | } 124 | 125 | /** 126 | * Gets email stats 127 | * 128 | * @see https://developers.convertkit.com/v4.html#get-email-stats 129 | * 130 | * @return false|mixed 131 | */ 132 | public function get_email_stats() 133 | { 134 | return $this->get('account/email_stats'); 135 | } 136 | 137 | /** 138 | * Gets growth stats 139 | * 140 | * @param \DateTime|null $starting Gets stats for time period beginning on this date. Defaults to 90 days ago. 141 | * @param \DateTime|null $ending Gets stats for time period ending on this date. Defaults to today. 142 | * 143 | * @see https://developers.convertkit.com/v4.html#get-growth-stats 144 | * 145 | * @return false|mixed 146 | */ 147 | public function get_growth_stats(\DateTime|null $starting = null, \DateTime|null $ending = null) 148 | { 149 | return $this->get( 150 | 'account/growth_stats', 151 | [ 152 | 'starting' => (!is_null($starting) ? $starting->format('Y-m-d') : ''), 153 | 'ending' => (!is_null($ending) ? $ending->format('Y-m-d') : ''), 154 | ] 155 | ); 156 | } 157 | 158 | /** 159 | * Get forms. 160 | * 161 | * @param string $status Form status (active|archived|trashed|all). 162 | * @param boolean $include_total_count To include the total count of records in the response, use true. 163 | * @param string $after_cursor Return results after the given pagination cursor. 164 | * @param string $before_cursor Return results before the given pagination cursor. 165 | * @param integer $per_page Number of results to return. 166 | * 167 | * @since 1.0.0 168 | * 169 | * @see https://developers.convertkit.com/v4.html#convertkit-api-forms 170 | * 171 | * @return mixed|array 172 | */ 173 | public function get_forms( 174 | string $status = 'active', 175 | bool $include_total_count = false, 176 | string $after_cursor = '', 177 | string $before_cursor = '', 178 | int $per_page = 100 179 | ) { 180 | return $this->get( 181 | 'forms', 182 | $this->build_total_count_and_pagination_params( 183 | [ 184 | 'type' => 'embed', 185 | 'status' => $status, 186 | ], 187 | $include_total_count, 188 | $after_cursor, 189 | $before_cursor, 190 | $per_page 191 | ) 192 | ); 193 | } 194 | 195 | /** 196 | * Get landing pages. 197 | * 198 | * @param string $status Form status (active|archived|trashed|all). 199 | * @param boolean $include_total_count To include the total count of records in the response, use true. 200 | * @param string $after_cursor Return results after the given pagination cursor. 201 | * @param string $before_cursor Return results before the given pagination cursor. 202 | * @param integer $per_page Number of results to return. 203 | * 204 | * @since 1.0.0 205 | * 206 | * @see https://developers.convertkit.com/v4.html#convertkit-api-forms 207 | * 208 | * @return mixed|array 209 | */ 210 | public function get_landing_pages( 211 | string $status = 'active', 212 | bool $include_total_count = false, 213 | string $after_cursor = '', 214 | string $before_cursor = '', 215 | int $per_page = 100 216 | ) { 217 | return $this->get( 218 | 'forms', 219 | $this->build_total_count_and_pagination_params( 220 | [ 221 | 'type' => 'hosted', 222 | 'status' => $status, 223 | ], 224 | $include_total_count, 225 | $after_cursor, 226 | $before_cursor, 227 | $per_page 228 | ) 229 | ); 230 | } 231 | 232 | /** 233 | * Adds subscribers to forms in bulk. 234 | * 235 | * @param array> $forms_subscribers_ids Array of arrays comprising of `form_id`, `subscriber_id` and optional `referrer` URL. 236 | * @param string $callback_url URL to notify for large batch size when async processing complete. 237 | * 238 | * @since 2.1.0 239 | * 240 | * @see https://developers.kit.com/v4.html#bulk-add-subscribers-to-forms 241 | * 242 | * @return mixed|object 243 | */ 244 | public function add_subscribers_to_forms(array $forms_subscribers_ids, string $callback_url = '') 245 | { 246 | // Build parameters. 247 | $options = ['additions' => $forms_subscribers_ids]; 248 | if (!empty($callback_url)) { 249 | $options['callback_url'] = $callback_url; 250 | } 251 | 252 | // Send request. 253 | return $this->post( 254 | 'bulk/forms/subscribers', 255 | $options 256 | ); 257 | } 258 | 259 | /** 260 | * Adds a subscriber to a form by email address 261 | * 262 | * @param integer $form_id Form ID. 263 | * @param string $email_address Email Address. 264 | * @param string $referrer Referrer. 265 | * 266 | * @see https://developers.convertkit.com/v4.html#add-subscriber-to-form-by-email-address 267 | * 268 | * @return false|mixed 269 | */ 270 | public function add_subscriber_to_form_by_email(int $form_id, string $email_address, string $referrer = '') 271 | { 272 | // Build parameters. 273 | $options = ['email_address' => $email_address]; 274 | 275 | if (!empty($referrer)) { 276 | $options['referrer'] = $referrer; 277 | } 278 | 279 | // Send request. 280 | return $this->post( 281 | sprintf('forms/%s/subscribers', $form_id), 282 | $options 283 | ); 284 | } 285 | 286 | /** 287 | * Adds a subscriber to a form by subscriber ID 288 | * 289 | * @param integer $form_id Form ID. 290 | * @param integer $subscriber_id Subscriber ID. 291 | * @param string $referrer Referrer URL. 292 | * 293 | * @see https://developers.convertkit.com/v4.html#add-subscriber-to-form 294 | * 295 | * @since 2.0.0 296 | * 297 | * @return false|mixed 298 | */ 299 | public function add_subscriber_to_form(int $form_id, int $subscriber_id, string $referrer = '') 300 | { 301 | // Build parameters. 302 | $options = []; 303 | 304 | if (!empty($referrer)) { 305 | $options['referrer'] = $referrer; 306 | } 307 | 308 | // Send request. 309 | return $this->post( 310 | sprintf('forms/%s/subscribers/%s', $form_id, $subscriber_id), 311 | $options 312 | ); 313 | } 314 | 315 | /** 316 | * List subscribers for a form 317 | * 318 | * @param integer $form_id Form ID. 319 | * @param string $subscriber_state Subscriber State (active|bounced|cancelled|complained|inactive). 320 | * @param \DateTime|null $created_after Filter subscribers who have been created after this date. 321 | * @param \DateTime|null $created_before Filter subscribers who have been created before this date. 322 | * @param \DateTime|null $added_after Filter subscribers who have been added to the form after this date. 323 | * @param \DateTime|null $added_before Filter subscribers who have been added to the form before this date. 324 | * @param boolean $include_total_count To include the total count of records in the response, use true. 325 | * @param string $after_cursor Return results after the given pagination cursor. 326 | * @param string $before_cursor Return results before the given pagination cursor. 327 | * @param integer $per_page Number of results to return. 328 | * 329 | * @see https://developers.convertkit.com/v4.html#list-subscribers-for-a-form 330 | * 331 | * @return false|mixed 332 | */ 333 | public function get_form_subscriptions( 334 | int $form_id, 335 | string $subscriber_state = 'active', 336 | \DateTime|null $created_after = null, 337 | \DateTime|null $created_before = null, 338 | \DateTime|null $added_after = null, 339 | \DateTime|null $added_before = null, 340 | bool $include_total_count = false, 341 | string $after_cursor = '', 342 | string $before_cursor = '', 343 | int $per_page = 100 344 | ) { 345 | // Build parameters. 346 | $options = []; 347 | 348 | if (!empty($subscriber_state)) { 349 | $options['status'] = $subscriber_state; 350 | } 351 | if (!is_null($created_after)) { 352 | $options['created_after'] = $created_after->format('Y-m-d'); 353 | } 354 | if (!is_null($created_before)) { 355 | $options['created_before'] = $created_before->format('Y-m-d'); 356 | } 357 | if (!is_null($added_after)) { 358 | $options['added_after'] = $added_after->format('Y-m-d'); 359 | } 360 | if (!is_null($added_before)) { 361 | $options['added_before'] = $added_before->format('Y-m-d'); 362 | } 363 | 364 | // Send request. 365 | return $this->get( 366 | sprintf('forms/%s/subscribers', $form_id), 367 | $this->build_total_count_and_pagination_params( 368 | $options, 369 | $include_total_count, 370 | $after_cursor, 371 | $before_cursor, 372 | $per_page 373 | ) 374 | ); 375 | } 376 | 377 | /** 378 | * Gets sequences 379 | * 380 | * @param boolean $include_total_count To include the total count of records in the response, use true. 381 | * @param string $after_cursor Return results after the given pagination cursor. 382 | * @param string $before_cursor Return results before the given pagination cursor. 383 | * @param integer $per_page Number of results to return. 384 | * 385 | * @see https://developers.convertkit.com/v4.html#list-sequences 386 | * 387 | * @return false|mixed 388 | */ 389 | public function get_sequences( 390 | bool $include_total_count = false, 391 | string $after_cursor = '', 392 | string $before_cursor = '', 393 | int $per_page = 100 394 | ) { 395 | return $this->get( 396 | 'sequences', 397 | $this->build_total_count_and_pagination_params( 398 | [], 399 | $include_total_count, 400 | $after_cursor, 401 | $before_cursor, 402 | $per_page 403 | ) 404 | ); 405 | } 406 | 407 | /** 408 | * Adds a subscriber to a sequence by email address 409 | * 410 | * @param integer $sequence_id Sequence ID. 411 | * @param string $email_address Email Address. 412 | * 413 | * @see https://developers.convertkit.com/v4.html#add-subscriber-to-sequence-by-email-address 414 | * 415 | * @return false|mixed 416 | */ 417 | public function add_subscriber_to_sequence_by_email(int $sequence_id, string $email_address) 418 | { 419 | return $this->post( 420 | sprintf('sequences/%s/subscribers', $sequence_id), 421 | ['email_address' => $email_address] 422 | ); 423 | } 424 | 425 | /** 426 | * Adds a subscriber to a sequence by subscriber ID 427 | * 428 | * @param integer $sequence_id Sequence ID. 429 | * @param integer $subscriber_id Subscriber ID. 430 | * 431 | * @see https://developers.convertkit.com/v4.html#add-subscriber-to-sequence 432 | * 433 | * @since 2.0.0 434 | * 435 | * @return false|mixed 436 | */ 437 | public function add_subscriber_to_sequence(int $sequence_id, int $subscriber_id) 438 | { 439 | return $this->post(sprintf('sequences/%s/subscribers/%s', $sequence_id, $subscriber_id)); 440 | } 441 | 442 | /** 443 | * List subscribers for a sequence 444 | * 445 | * @param integer $sequence_id Sequence ID. 446 | * @param string $subscriber_state Subscriber State (active|bounced|cancelled|complained|inactive). 447 | * @param \DateTime|null $created_after Filter subscribers who have been created after this date. 448 | * @param \DateTime|null $created_before Filter subscribers who have been created before this date. 449 | * @param \DateTime|null $added_after Filter subscribers who have been added to the form after this date. 450 | * @param \DateTime|null $added_before Filter subscribers who have been added to the form before this date. 451 | * @param boolean $include_total_count To include the total count of records in the response, use true. 452 | * @param string $after_cursor Return results after the given pagination cursor. 453 | * @param string $before_cursor Return results before the given pagination cursor. 454 | * @param integer $per_page Number of results to return. 455 | * 456 | * @see https://developers.convertkit.com/v4.html#list-subscribers-for-a-sequence 457 | * 458 | * @return false|mixed 459 | */ 460 | public function get_sequence_subscriptions( 461 | int $sequence_id, 462 | string $subscriber_state = 'active', 463 | \DateTime|null $created_after = null, 464 | \DateTime|null $created_before = null, 465 | \DateTime|null $added_after = null, 466 | \DateTime|null $added_before = null, 467 | bool $include_total_count = false, 468 | string $after_cursor = '', 469 | string $before_cursor = '', 470 | int $per_page = 100 471 | ) { 472 | // Build parameters. 473 | $options = []; 474 | 475 | if (!empty($subscriber_state)) { 476 | $options['status'] = $subscriber_state; 477 | } 478 | if (!is_null($created_after)) { 479 | $options['created_after'] = $created_after->format('Y-m-d'); 480 | } 481 | if (!is_null($created_before)) { 482 | $options['created_before'] = $created_before->format('Y-m-d'); 483 | } 484 | if (!is_null($added_after)) { 485 | $options['added_after'] = $added_after->format('Y-m-d'); 486 | } 487 | if (!is_null($added_before)) { 488 | $options['added_before'] = $added_before->format('Y-m-d'); 489 | } 490 | 491 | // Send request. 492 | return $this->get( 493 | sprintf('sequences/%s/subscribers', $sequence_id), 494 | $this->build_total_count_and_pagination_params( 495 | $options, 496 | $include_total_count, 497 | $after_cursor, 498 | $before_cursor, 499 | $per_page 500 | ) 501 | ); 502 | } 503 | 504 | /** 505 | * List tags. 506 | * 507 | * @param boolean $include_total_count To include the total count of records in the response, use true. 508 | * @param string $after_cursor Return results after the given pagination cursor. 509 | * @param string $before_cursor Return results before the given pagination cursor. 510 | * @param integer $per_page Number of results to return. 511 | * 512 | * @see https://developers.convertkit.com/v4.html#list-tags 513 | * 514 | * @return mixed|array 515 | */ 516 | public function get_tags( 517 | bool $include_total_count = false, 518 | string $after_cursor = '', 519 | string $before_cursor = '', 520 | int $per_page = 100 521 | ) { 522 | return $this->get( 523 | 'tags', 524 | $this->build_total_count_and_pagination_params( 525 | [], 526 | $include_total_count, 527 | $after_cursor, 528 | $before_cursor, 529 | $per_page 530 | ) 531 | ); 532 | } 533 | 534 | /** 535 | * Creates a tag. 536 | * 537 | * @param string $tag Tag Name. 538 | * 539 | * @since 1.0.0 540 | * 541 | * @see https://developers.convertkit.com/v4.html#create-a-tag 542 | * 543 | * @return false|mixed 544 | */ 545 | public function create_tag(string $tag) 546 | { 547 | return $this->post( 548 | 'tags', 549 | ['name' => $tag] 550 | ); 551 | } 552 | 553 | /** 554 | * Creates multiple tags. 555 | * 556 | * @param array $tags Tag Names. 557 | * @param string $callback_url URL to notify for large batch size when async processing complete. 558 | * 559 | * @since 1.1.0 560 | * 561 | * @see https://developers.convertkit.com/v4.html#bulk-create-tags 562 | * 563 | * @return false|mixed 564 | */ 565 | public function create_tags(array $tags, string $callback_url = '') 566 | { 567 | // Build parameters. 568 | $options = [ 569 | 'tags' => [], 570 | ]; 571 | foreach ($tags as $i => $tag) { 572 | $options['tags'][] = [ 573 | 'name' => (string) $tag, 574 | ]; 575 | } 576 | 577 | if (!empty($callback_url)) { 578 | $options['callback_url'] = $callback_url; 579 | } 580 | 581 | // Send request. 582 | return $this->post( 583 | 'bulk/tags', 584 | $options 585 | ); 586 | } 587 | 588 | /** 589 | * Tags a subscriber with the given existing Tag. 590 | * 591 | * @param integer $tag_id Tag ID. 592 | * @param string $email_address Email Address. 593 | * 594 | * @see https://developers.convertkit.com/v4.html#tag-a-subscriber-by-email-address 595 | * 596 | * @return false|mixed 597 | */ 598 | public function tag_subscriber_by_email(int $tag_id, string $email_address) 599 | { 600 | return $this->post( 601 | sprintf('tags/%s/subscribers', $tag_id), 602 | ['email_address' => $email_address] 603 | ); 604 | } 605 | 606 | /** 607 | * Tags a subscriber by subscriber ID with the given existing Tag. 608 | * 609 | * @param integer $tag_id Tag ID. 610 | * @param integer $subscriber_id Subscriber ID. 611 | * 612 | * @see https://developers.convertkit.com/v4.html#tag-a-subscriber 613 | * 614 | * @return false|mixed 615 | */ 616 | public function tag_subscriber(int $tag_id, int $subscriber_id) 617 | { 618 | return $this->post(sprintf('tags/%s/subscribers/%s', $tag_id, $subscriber_id)); 619 | } 620 | 621 | /** 622 | * Removes a tag from a subscriber. 623 | * 624 | * @param integer $tag_id Tag ID. 625 | * @param integer $subscriber_id Subscriber ID. 626 | * 627 | * @since 1.0.0 628 | * 629 | * @see https://developers.convertkit.com/v4.html#remove-tag-from-subscriber 630 | * 631 | * @return false|mixed 632 | */ 633 | public function remove_tag_from_subscriber(int $tag_id, int $subscriber_id) 634 | { 635 | return $this->delete(sprintf('tags/%s/subscribers/%s', $tag_id, $subscriber_id)); 636 | } 637 | 638 | /** 639 | * Removes a tag from a subscriber by email address. 640 | * 641 | * @param integer $tag_id Tag ID. 642 | * @param string $email_address Subscriber email address. 643 | * 644 | * @since 1.0.0 645 | * 646 | * @see https://developers.convertkit.com/v4.html#remove-tag-from-subscriber-by-email-address 647 | * 648 | * @return false|mixed 649 | */ 650 | public function remove_tag_from_subscriber_by_email(int $tag_id, string $email_address) 651 | { 652 | return $this->delete( 653 | sprintf('tags/%s/subscribers', $tag_id), 654 | ['email_address' => $email_address] 655 | ); 656 | } 657 | 658 | /** 659 | * List subscribers for a tag 660 | * 661 | * @param integer $tag_id Tag ID. 662 | * @param string $subscriber_state Subscriber State (active|bounced|cancelled|complained|inactive). 663 | * @param \DateTime|null $created_after Filter subscribers who have been created after this date. 664 | * @param \DateTime|null $created_before Filter subscribers who have been created before this date. 665 | * @param \DateTime|null $tagged_after Filter subscribers who have been tagged after this date. 666 | * @param \DateTime|null $tagged_before Filter subscribers who have been tagged before this date. 667 | * @param boolean $include_total_count To include the total count of records in the response, use true. 668 | * @param string $after_cursor Return results after the given pagination cursor. 669 | * @param string $before_cursor Return results before the given pagination cursor. 670 | * @param integer $per_page Number of results to return. 671 | * 672 | * @see https://developers.convertkit.com/v4.html#list-subscribers-for-a-tag 673 | * 674 | * @return false|mixed 675 | */ 676 | public function get_tag_subscriptions( 677 | int $tag_id, 678 | string $subscriber_state = 'active', 679 | \DateTime|null $created_after = null, 680 | \DateTime|null $created_before = null, 681 | \DateTime|null $tagged_after = null, 682 | \DateTime|null $tagged_before = null, 683 | bool $include_total_count = false, 684 | string $after_cursor = '', 685 | string $before_cursor = '', 686 | int $per_page = 100 687 | ) { 688 | // Build parameters. 689 | $options = []; 690 | 691 | if (!empty($subscriber_state)) { 692 | $options['status'] = $subscriber_state; 693 | } 694 | if (!is_null($created_after)) { 695 | $options['created_after'] = $created_after->format('Y-m-d'); 696 | } 697 | if (!is_null($created_before)) { 698 | $options['created_before'] = $created_before->format('Y-m-d'); 699 | } 700 | if (!is_null($tagged_after)) { 701 | $options['tagged_after'] = $tagged_after->format('Y-m-d'); 702 | } 703 | if (!is_null($tagged_before)) { 704 | $options['tagged_before'] = $tagged_before->format('Y-m-d'); 705 | } 706 | 707 | // Send request. 708 | return $this->get( 709 | sprintf('tags/%s/subscribers', $tag_id), 710 | $this->build_total_count_and_pagination_params( 711 | $options, 712 | $include_total_count, 713 | $after_cursor, 714 | $before_cursor, 715 | $per_page 716 | ) 717 | ); 718 | } 719 | 720 | /** 721 | * List email templates. 722 | * 723 | * @param boolean $include_total_count To include the total count of records in the response, use true. 724 | * @param string $after_cursor Return results after the given pagination cursor. 725 | * @param string $before_cursor Return results before the given pagination cursor. 726 | * @param integer $per_page Number of results to return. 727 | * 728 | * @since 2.0.0 729 | * 730 | * @see https://developers.convertkit.com/v4.html#convertkit-api-email-templates 731 | * 732 | * @return false|mixed 733 | */ 734 | public function get_email_templates( 735 | bool $include_total_count = false, 736 | string $after_cursor = '', 737 | string $before_cursor = '', 738 | int $per_page = 100 739 | ) { 740 | // Send request. 741 | return $this->get( 742 | 'email_templates', 743 | $this->build_total_count_and_pagination_params( 744 | [], 745 | $include_total_count, 746 | $after_cursor, 747 | $before_cursor, 748 | $per_page 749 | ) 750 | ); 751 | } 752 | 753 | /** 754 | * List subscribers. 755 | * 756 | * @param string $subscriber_state Subscriber State (active|bounced|cancelled|complained|inactive). 757 | * @param string $email_address Search susbcribers by email address. This is an exact match search. 758 | * @param \DateTime|null $created_after Filter subscribers who have been created after this date. 759 | * @param \DateTime|null $created_before Filter subscribers who have been created before this date. 760 | * @param \DateTime|null $updated_after Filter subscribers who have been updated after this date. 761 | * @param \DateTime|null $updated_before Filter subscribers who have been updated before this date. 762 | * @param string $sort_field Sort Field (id|updated_at|cancelled_at). 763 | * @param string $sort_order Sort Order (asc|desc). 764 | * @param boolean $include_total_count To include the total count of records in the response, use true. 765 | * @param string $after_cursor Return results after the given pagination cursor. 766 | * @param string $before_cursor Return results before the given pagination cursor. 767 | * @param integer $per_page Number of results to return. 768 | * 769 | * @since 2.0.0 770 | * 771 | * @see https://developers.convertkit.com/v4.html#list-subscribers 772 | * 773 | * @return false|mixed 774 | */ 775 | public function get_subscribers( 776 | string $subscriber_state = 'active', 777 | string $email_address = '', 778 | \DateTime|null $created_after = null, 779 | \DateTime|null $created_before = null, 780 | \DateTime|null $updated_after = null, 781 | \DateTime|null $updated_before = null, 782 | string $sort_field = 'id', 783 | string $sort_order = 'desc', 784 | bool $include_total_count = false, 785 | string $after_cursor = '', 786 | string $before_cursor = '', 787 | int $per_page = 100 788 | ) { 789 | // Build parameters. 790 | $options = []; 791 | 792 | if (!empty($subscriber_state)) { 793 | $options['status'] = $subscriber_state; 794 | } 795 | if (!empty($email_address)) { 796 | $options['email_address'] = $email_address; 797 | } 798 | if (!is_null($created_after)) { 799 | $options['created_after'] = $created_after->format('Y-m-d'); 800 | } 801 | if (!is_null($created_before)) { 802 | $options['created_before'] = $created_before->format('Y-m-d'); 803 | } 804 | if (!is_null($updated_after)) { 805 | $options['updated_after'] = $updated_after->format('Y-m-d'); 806 | } 807 | if (!is_null($updated_before)) { 808 | $options['updated_before'] = $updated_before->format('Y-m-d'); 809 | } 810 | if (!empty($sort_field)) { 811 | $options['sort_field'] = $sort_field; 812 | } 813 | if (!empty($sort_order)) { 814 | $options['sort_order'] = $sort_order; 815 | } 816 | 817 | // Send request. 818 | return $this->get( 819 | 'subscribers', 820 | $this->build_total_count_and_pagination_params( 821 | $options, 822 | $include_total_count, 823 | $after_cursor, 824 | $before_cursor, 825 | $per_page 826 | ) 827 | ); 828 | } 829 | 830 | /** 831 | * Create a subscriber. 832 | * 833 | * Behaves as an upsert. If a subscriber with the provided email address does not exist, 834 | * it creates one with the specified first name and state. If a subscriber with the provided 835 | * email address already exists, it updates the first name. 836 | * 837 | * @param string $email_address Email Address. 838 | * @param string $first_name First Name. 839 | * @param string $subscriber_state Subscriber State (active|bounced|cancelled|complained|inactive). 840 | * @param array $fields Custom Fields. 841 | * 842 | * @since 2.0.0 843 | * 844 | * @see https://developers.convertkit.com/v4.html#create-a-subscriber 845 | * 846 | * @return mixed 847 | */ 848 | public function create_subscriber( 849 | string $email_address, 850 | string $first_name = '', 851 | string $subscriber_state = '', 852 | array $fields = [] 853 | ) { 854 | // Build parameters. 855 | $options = ['email_address' => $email_address]; 856 | 857 | if (!empty($first_name)) { 858 | $options['first_name'] = $first_name; 859 | } 860 | if (!empty($subscriber_state)) { 861 | $options['state'] = $subscriber_state; 862 | } 863 | if (count($fields)) { 864 | $options['fields'] = $fields; 865 | } 866 | 867 | // Send request. 868 | return $this->post( 869 | 'subscribers', 870 | $options 871 | ); 872 | } 873 | 874 | /** 875 | * Create multiple subscribers. 876 | * 877 | * @param array> $subscribers Subscribers. 878 | * @param string $callback_url URL to notify for large batch size when async processing complete. 879 | * 880 | * @since 2.0.0 881 | * 882 | * @see https://developers.convertkit.com/v4.html#bulk-create-subscribers 883 | * 884 | * @return mixed 885 | */ 886 | public function create_subscribers(array $subscribers, string $callback_url = '') 887 | { 888 | // Build parameters. 889 | $options = ['subscribers' => $subscribers]; 890 | 891 | if (!empty($callback_url)) { 892 | $options['callback_url'] = $callback_url; 893 | } 894 | 895 | // Send request. 896 | return $this->post( 897 | 'bulk/subscribers', 898 | $options 899 | ); 900 | } 901 | 902 | /** 903 | * Get the ConvertKit subscriber ID associated with email address if it exists. 904 | * Return false if subscriber not found. 905 | * 906 | * @param string $email_address Email Address. 907 | * 908 | * @throws \InvalidArgumentException If the email address is not a valid email format. 909 | * 910 | * @see https://developers.convertkit.com/v4.html#get-a-subscriber 911 | * 912 | * @return false|integer 913 | */ 914 | public function get_subscriber_id(string $email_address) 915 | { 916 | $subscribers = $this->get( 917 | 'subscribers', 918 | ['email_address' => $email_address] 919 | ); 920 | 921 | if (!$subscribers instanceof \stdClass) { 922 | return false; 923 | } 924 | 925 | if (!is_array($subscribers->subscribers)) { 926 | return false; 927 | } 928 | 929 | if (!count($subscribers->subscribers)) { 930 | return false; 931 | } 932 | 933 | if (!$subscribers->subscribers[0] instanceof \stdClass) { 934 | return false; 935 | } 936 | 937 | if (!is_int($subscribers->subscribers[0]->id)) { 938 | return false; 939 | } 940 | 941 | // Return the subscriber's ID. 942 | return $subscribers->subscribers[0]->id; 943 | } 944 | 945 | /** 946 | * Get subscriber by id 947 | * 948 | * @param integer $subscriber_id Subscriber ID. 949 | * 950 | * @see https://developers.convertkit.com/v4.html#get-a-subscriber 951 | * 952 | * @return mixed|integer 953 | */ 954 | public function get_subscriber(int $subscriber_id) 955 | { 956 | return $this->get(sprintf('subscribers/%s', $subscriber_id)); 957 | } 958 | 959 | /** 960 | * Updates the information for a single subscriber. 961 | * 962 | * @param integer $subscriber_id Existing Subscriber ID. 963 | * @param string $first_name New First Name. 964 | * @param string $email_address New Email Address. 965 | * @param array $fields Updated Custom Fields. 966 | * 967 | * @see https://developers.convertkit.com/v4.html#update-a-subscriber 968 | * 969 | * @return mixed 970 | */ 971 | public function update_subscriber( 972 | int $subscriber_id, 973 | string $first_name = '', 974 | string $email_address = '', 975 | array $fields = [] 976 | ) { 977 | // Build parameters. 978 | $options = []; 979 | 980 | if (!empty($first_name)) { 981 | $options['first_name'] = $first_name; 982 | } 983 | if (!empty($email_address)) { 984 | $options['email_address'] = $email_address; 985 | } 986 | if (!empty($fields)) { 987 | $options['fields'] = $fields; 988 | } 989 | 990 | // Send request. 991 | return $this->put( 992 | sprintf('subscribers/%s', $subscriber_id), 993 | $options 994 | ); 995 | } 996 | 997 | /** 998 | * Unsubscribe an email address. 999 | * 1000 | * @param string $email_address Email Address. 1001 | * 1002 | * @see https://developers.convertkit.com/v4.html#unsubscribe-subscriber 1003 | * 1004 | * @return mixed|object 1005 | */ 1006 | public function unsubscribe_by_email(string $email_address) 1007 | { 1008 | return $this->post( 1009 | sprintf( 1010 | 'subscribers/%s/unsubscribe', 1011 | $this->get_subscriber_id($email_address) 1012 | ) 1013 | ); 1014 | } 1015 | 1016 | /** 1017 | * Unsubscribe the given subscriber ID. 1018 | * 1019 | * @param integer $subscriber_id Subscriber ID. 1020 | * 1021 | * @see https://developers.convertkit.com/v4.html#unsubscribe-subscriber 1022 | * 1023 | * @return mixed|object 1024 | */ 1025 | public function unsubscribe(int $subscriber_id) 1026 | { 1027 | return $this->post(sprintf('subscribers/%s/unsubscribe', $subscriber_id)); 1028 | } 1029 | 1030 | /** 1031 | * Get a list of the tags for a subscriber. 1032 | * 1033 | * @param integer $subscriber_id Subscriber ID. 1034 | * @param boolean $include_total_count To include the total count of records in the response, use true. 1035 | * @param string $after_cursor Return results after the given pagination cursor. 1036 | * @param string $before_cursor Return results before the given pagination cursor. 1037 | * @param integer $per_page Number of results to return. 1038 | * 1039 | * @see https://developers.convertkit.com/v4.html#list-tags-for-a-subscriber 1040 | * 1041 | * @return mixed|array 1042 | */ 1043 | public function get_subscriber_tags( 1044 | int $subscriber_id, 1045 | bool $include_total_count = false, 1046 | string $after_cursor = '', 1047 | string $before_cursor = '', 1048 | int $per_page = 100 1049 | ) { 1050 | return $this->get( 1051 | sprintf('subscribers/%s/tags', $subscriber_id), 1052 | $this->build_total_count_and_pagination_params( 1053 | [], 1054 | $include_total_count, 1055 | $after_cursor, 1056 | $before_cursor, 1057 | $per_page 1058 | ) 1059 | ); 1060 | } 1061 | 1062 | /** 1063 | * List broadcasts. 1064 | * 1065 | * @param boolean $include_total_count To include the total count of records in the response, use true. 1066 | * @param string $after_cursor Return results after the given pagination cursor. 1067 | * @param string $before_cursor Return results before the given pagination cursor. 1068 | * @param integer $per_page Number of results to return. 1069 | * 1070 | * @see https://developers.convertkit.com/v4.html#list-broadcasts 1071 | * 1072 | * @return false|mixed 1073 | */ 1074 | public function get_broadcasts( 1075 | bool $include_total_count = false, 1076 | string $after_cursor = '', 1077 | string $before_cursor = '', 1078 | int $per_page = 100 1079 | ) { 1080 | // Send request. 1081 | return $this->get( 1082 | 'broadcasts', 1083 | $this->build_total_count_and_pagination_params( 1084 | [], 1085 | $include_total_count, 1086 | $after_cursor, 1087 | $before_cursor, 1088 | $per_page 1089 | ) 1090 | ); 1091 | } 1092 | 1093 | /** 1094 | * Creates a broadcast. 1095 | * 1096 | * @param string $subject The broadcast email's subject. 1097 | * @param string $content The broadcast's email HTML content. 1098 | * @param string $description An internal description of this broadcast. 1099 | * @param boolean $public Specifies whether or not this is a public post. 1100 | * @param \DateTime|null $published_at Specifies the time that this post was published (applicable 1101 | * only to public posts). 1102 | * @param \DateTime|null $send_at Time that this broadcast should be sent; leave blank to create 1103 | * a draft broadcast. If set to a future time, this is the time that 1104 | * the broadcast will be scheduled to send. 1105 | * @param string $email_address Sending email address; leave blank to use your account's 1106 | * default sending email address. 1107 | * @param string $email_template_id ID of the email template to use; leave blank to use your 1108 | * account's default email template. 1109 | * @param string $thumbnail_alt Specify the ALT attribute of the public thumbnail image 1110 | * (applicable only to public posts). 1111 | * @param string $thumbnail_url Specify the URL of the thumbnail image to accompany the broadcast 1112 | * post (applicable only to public posts). 1113 | * @param string $preview_text Specify the preview text of the email. 1114 | * @param array $subscriber_filter Filter subscriber(s) to send the email to. 1115 | * 1116 | * @see https://developers.convertkit.com/v4.html#create-a-broadcast 1117 | * 1118 | * @return mixed|object 1119 | */ 1120 | public function create_broadcast( 1121 | string $subject = '', 1122 | string $content = '', 1123 | string $description = '', 1124 | bool $public = false, 1125 | \DateTime|null $published_at = null, 1126 | \DateTime|null $send_at = null, 1127 | string $email_address = '', 1128 | string $email_template_id = '', 1129 | string $thumbnail_alt = '', 1130 | string $thumbnail_url = '', 1131 | string $preview_text = '', 1132 | array $subscriber_filter = [] 1133 | ) { 1134 | $options = [ 1135 | 'email_template_id' => $email_template_id, 1136 | 'email_address' => $email_address, 1137 | 'content' => $content, 1138 | 'description' => $description, 1139 | 'public' => $public, 1140 | 'published_at' => (!is_null($published_at) ? $published_at->format('Y-m-d H:i:s') : ''), 1141 | 'send_at' => (!is_null($send_at) ? $send_at->format('Y-m-d H:i:s') : ''), 1142 | 'thumbnail_alt' => $thumbnail_alt, 1143 | 'thumbnail_url' => $thumbnail_url, 1144 | 'preview_text' => $preview_text, 1145 | 'subject' => $subject, 1146 | ]; 1147 | if (count($subscriber_filter)) { 1148 | $options['subscriber_filter'] = $subscriber_filter; 1149 | } 1150 | 1151 | // Iterate through options, removing blank entries. 1152 | foreach ($options as $key => $value) { 1153 | if (is_string($value) && strlen($value) === 0) { 1154 | unset($options[$key]); 1155 | } 1156 | } 1157 | 1158 | // If the post isn't public, remove some options that don't apply. 1159 | if (!$public) { 1160 | unset($options['published_at'], $options['thumbnail_alt'], $options['thumbnail_url']); 1161 | } 1162 | 1163 | // Send request. 1164 | return $this->post( 1165 | 'broadcasts', 1166 | $options 1167 | ); 1168 | } 1169 | 1170 | /** 1171 | * Retrieve a specific broadcast. 1172 | * 1173 | * @param integer $id Broadcast ID. 1174 | * 1175 | * @see https://developers.convertkit.com/v4.html#get-a-broadcast 1176 | * 1177 | * @return mixed|object 1178 | */ 1179 | public function get_broadcast(int $id) 1180 | { 1181 | return $this->get(sprintf('broadcasts/%s', $id)); 1182 | } 1183 | 1184 | /** 1185 | * Get the statistics (recipient count, open rate, click rate, unsubscribe count, 1186 | * total clicks, status, and send progress) for a specific broadcast. 1187 | * 1188 | * @param integer $id Broadcast ID. 1189 | * 1190 | * @see https://developers.convertkit.com/v4.html#get-stats 1191 | * 1192 | * @return mixed|object 1193 | */ 1194 | public function get_broadcast_stats(int $id) 1195 | { 1196 | return $this->get(sprintf('broadcasts/%s/stats', $id)); 1197 | } 1198 | 1199 | /** 1200 | * Updates a broadcast. 1201 | * 1202 | * @param integer $id Broadcast ID. 1203 | * @param string $subject The broadcast email's subject. 1204 | * @param string $content The broadcast's email HTML content. 1205 | * @param string $description An internal description of this broadcast. 1206 | * @param boolean $public Specifies whether or not this is a public post. 1207 | * @param \DateTime|null $published_at Specifies the time that this post was published (applicable 1208 | * only to public posts). 1209 | * @param \DateTime|null $send_at Time that this broadcast should be sent; leave blank to create 1210 | * a draft broadcast. If set to a future time, this is the time that 1211 | * the broadcast will be scheduled to send. 1212 | * @param string $email_address Sending email address; leave blank to use your account's 1213 | * default sending email address. 1214 | * @param string $email_template_id ID of the email template to use; leave blank to use your 1215 | * account's default email template. 1216 | * @param string $thumbnail_alt Specify the ALT attribute of the public thumbnail image 1217 | * (applicable only to public posts). 1218 | * @param string $thumbnail_url Specify the URL of the thumbnail image to accompany the broadcast 1219 | * post (applicable only to public posts). 1220 | * @param string $preview_text Specify the preview text of the email. 1221 | * @param array $subscriber_filter Filter subscriber(s) to send the email to. 1222 | * 1223 | * @see https://developers.convertkit.com/#create-a-broadcast 1224 | * 1225 | * @return mixed|object 1226 | */ 1227 | public function update_broadcast( 1228 | int $id, 1229 | string $subject = '', 1230 | string $content = '', 1231 | string $description = '', 1232 | bool $public = false, 1233 | \DateTime|null $published_at = null, 1234 | \DateTime|null $send_at = null, 1235 | string $email_address = '', 1236 | string $email_template_id = '', 1237 | string $thumbnail_alt = '', 1238 | string $thumbnail_url = '', 1239 | string $preview_text = '', 1240 | array $subscriber_filter = [] 1241 | ) { 1242 | $options = [ 1243 | 'email_template_id' => $email_template_id, 1244 | 'email_address' => $email_address, 1245 | 'content' => $content, 1246 | 'description' => $description, 1247 | 'public' => $public, 1248 | 'published_at' => (!is_null($published_at) ? $published_at->format('Y-m-d H:i:s') : ''), 1249 | 'send_at' => (!is_null($send_at) ? $send_at->format('Y-m-d H:i:s') : ''), 1250 | 'thumbnail_alt' => $thumbnail_alt, 1251 | 'thumbnail_url' => $thumbnail_url, 1252 | 'preview_text' => $preview_text, 1253 | 'subject' => $subject, 1254 | ]; 1255 | if (count($subscriber_filter)) { 1256 | $options['subscriber_filter'] = $subscriber_filter; 1257 | } 1258 | 1259 | // Iterate through options, removing blank entries. 1260 | foreach ($options as $key => $value) { 1261 | if (is_string($value) && strlen($value) === 0) { 1262 | unset($options[$key]); 1263 | } 1264 | } 1265 | 1266 | // If the post isn't public, remove some options that don't apply. 1267 | if (!$public) { 1268 | unset($options['published_at'], $options['thumbnail_alt'], $options['thumbnail_url']); 1269 | } 1270 | 1271 | // Send request. 1272 | return $this->put( 1273 | sprintf('broadcasts/%s', $id), 1274 | $options 1275 | ); 1276 | } 1277 | 1278 | /** 1279 | * Deletes an existing broadcast. 1280 | * 1281 | * @param integer $id Broadcast ID. 1282 | * 1283 | * @since 1.0.0 1284 | * 1285 | * @see https://developers.convertkit.com/v4.html#delete-a-broadcast 1286 | * 1287 | * @return mixed|object 1288 | */ 1289 | public function delete_broadcast(int $id) 1290 | { 1291 | return $this->delete(sprintf('broadcasts/%s', $id)); 1292 | } 1293 | 1294 | /** 1295 | * List webhooks. 1296 | * 1297 | * @param boolean $include_total_count To include the total count of records in the response, use true. 1298 | * @param string $after_cursor Return results after the given pagination cursor. 1299 | * @param string $before_cursor Return results before the given pagination cursor. 1300 | * @param integer $per_page Number of results to return. 1301 | * 1302 | * @since 2.0.0 1303 | * 1304 | * @see https://developers.convertkit.com/v4.html#list-webhooks 1305 | * 1306 | * @return false|mixed 1307 | */ 1308 | public function get_webhooks( 1309 | bool $include_total_count = false, 1310 | string $after_cursor = '', 1311 | string $before_cursor = '', 1312 | int $per_page = 100 1313 | ) { 1314 | // Send request. 1315 | return $this->get( 1316 | 'webhooks', 1317 | $this->build_total_count_and_pagination_params( 1318 | [], 1319 | $include_total_count, 1320 | $after_cursor, 1321 | $before_cursor, 1322 | $per_page 1323 | ) 1324 | ); 1325 | } 1326 | 1327 | /** 1328 | * Creates a webhook that will be called based on the chosen event types. 1329 | * 1330 | * @param string $url URL to receive event. 1331 | * @param string $event Event to subscribe to. 1332 | * @param string $parameter Optional parameter depending on the event. 1333 | * 1334 | * @since 1.0.0 1335 | * 1336 | * @see https://developers.convertkit.com/v4.html#create-a-webhook 1337 | * 1338 | * @throws \InvalidArgumentException If the event is not supported. 1339 | * 1340 | * @return mixed|object 1341 | */ 1342 | public function create_webhook(string $url, string $event, string $parameter = '') 1343 | { 1344 | // Depending on the event, build the required event array structure. 1345 | switch ($event) { 1346 | case 'subscriber.subscriber_activate': 1347 | case 'subscriber.subscriber_unsubscribe': 1348 | case 'subscriber.subscriber_bounce': 1349 | case 'subscriber.subscriber_complain': 1350 | case 'purchase.purchase_create': 1351 | $eventData = ['name' => $event]; 1352 | break; 1353 | 1354 | case 'subscriber.form_subscribe': 1355 | $eventData = [ 1356 | 'name' => $event, 1357 | 'form_id' => $parameter, 1358 | ]; 1359 | break; 1360 | 1361 | case 'subscriber.course_subscribe': 1362 | case 'subscriber.course_complete': 1363 | $eventData = [ 1364 | 'name' => $event, 1365 | 'course_id' => $parameter, 1366 | ]; 1367 | break; 1368 | 1369 | case 'subscriber.link_click': 1370 | $eventData = [ 1371 | 'name' => $event, 1372 | 'initiator_value' => $parameter, 1373 | ]; 1374 | break; 1375 | 1376 | case 'subscriber.product_purchase': 1377 | $eventData = [ 1378 | 'name' => $event, 1379 | 'product_id' => $parameter, 1380 | ]; 1381 | break; 1382 | 1383 | case 'subscriber.tag_add': 1384 | case 'subscriber.tag_remove': 1385 | $eventData = [ 1386 | 'name' => $event, 1387 | 'tag_id' => $parameter, 1388 | ]; 1389 | break; 1390 | 1391 | default: 1392 | throw new \InvalidArgumentException(sprintf('The event %s is not supported', $event)); 1393 | }//end switch 1394 | 1395 | // Send request. 1396 | return $this->post( 1397 | 'webhooks', 1398 | [ 1399 | 'target_url' => $url, 1400 | 'event' => $eventData, 1401 | ] 1402 | ); 1403 | } 1404 | 1405 | /** 1406 | * Deletes an existing webhook. 1407 | * 1408 | * @param integer $id Webhook ID. 1409 | * 1410 | * @since 1.0.0 1411 | * 1412 | * @see https://developers.convertkit.com/v4.html#delete-a-webhook 1413 | * 1414 | * @return mixed|object 1415 | */ 1416 | public function delete_webhook(int $id) 1417 | { 1418 | return $this->delete(sprintf('webhooks/%s', $id)); 1419 | } 1420 | 1421 | /** 1422 | * List custom fields. 1423 | * 1424 | * @param boolean $include_total_count To include the total count of records in the response, use true. 1425 | * @param string $after_cursor Return results after the given pagination cursor. 1426 | * @param string $before_cursor Return results before the given pagination cursor. 1427 | * @param integer $per_page Number of results to return. 1428 | * 1429 | * @since 1.0.0 1430 | * 1431 | * @see https://developers.convertkit.com/v4.html#list-custom-fields 1432 | * 1433 | * @return false|mixed 1434 | */ 1435 | public function get_custom_fields( 1436 | bool $include_total_count = false, 1437 | string $after_cursor = '', 1438 | string $before_cursor = '', 1439 | int $per_page = 100 1440 | ) { 1441 | // Send request. 1442 | return $this->get( 1443 | 'custom_fields', 1444 | $this->build_total_count_and_pagination_params( 1445 | [], 1446 | $include_total_count, 1447 | $after_cursor, 1448 | $before_cursor, 1449 | $per_page 1450 | ) 1451 | ); 1452 | } 1453 | 1454 | /** 1455 | * Creates a custom field. 1456 | * 1457 | * @param string $label Custom Field label. 1458 | * 1459 | * @since 1.0.0 1460 | * 1461 | * @see https://developers.convertkit.com/v4.html#create-a-custom-field 1462 | * 1463 | * @return mixed|object 1464 | */ 1465 | public function create_custom_field(string $label) 1466 | { 1467 | return $this->post( 1468 | 'custom_fields', 1469 | ['label' => $label] 1470 | ); 1471 | } 1472 | 1473 | /** 1474 | * Creates multiple custom fields. 1475 | * 1476 | * @param array $labels Custom Fields labels. 1477 | * @param string $callback_url URL to notify for large batch size when async processing complete. 1478 | * 1479 | * @since 1.0.0 1480 | * 1481 | * @see https://developers.convertkit.com/v4.html#bulk-create-custom-fields 1482 | * 1483 | * @return mixed|object 1484 | */ 1485 | public function create_custom_fields(array $labels, string $callback_url = '') 1486 | { 1487 | // Build parameters. 1488 | $options = [ 1489 | 'custom_fields' => [], 1490 | ]; 1491 | foreach ($labels as $i => $label) { 1492 | $options['custom_fields'][] = [ 1493 | 'label' => (string) $label, 1494 | ]; 1495 | } 1496 | 1497 | if (!empty($callback_url)) { 1498 | $options['callback_url'] = $callback_url; 1499 | } 1500 | 1501 | // Send request. 1502 | return $this->post( 1503 | 'bulk/custom_fields', 1504 | $options 1505 | ); 1506 | } 1507 | 1508 | /** 1509 | * Updates an existing custom field. 1510 | * 1511 | * @param integer $id Custom Field ID. 1512 | * @param string $label Updated Custom Field label. 1513 | * 1514 | * @since 1.0.0 1515 | * 1516 | * @see https://developers.convertkit.com/v4.html#update-a-custom-field 1517 | * 1518 | * @return mixed|object 1519 | */ 1520 | public function update_custom_field(int $id, string $label) 1521 | { 1522 | return $this->put( 1523 | sprintf('custom_fields/%s', $id), 1524 | ['label' => $label] 1525 | ); 1526 | } 1527 | 1528 | /** 1529 | * Deletes an existing custom field. 1530 | * 1531 | * @param integer $id Custom Field ID. 1532 | * 1533 | * @since 1.0.0 1534 | * 1535 | * @see https://developers.convertkit.com/#destroy-field 1536 | * 1537 | * @return mixed|object 1538 | */ 1539 | public function delete_custom_field(int $id) 1540 | { 1541 | return $this->delete(sprintf('custom_fields/%s', $id)); 1542 | } 1543 | 1544 | /** 1545 | * List purchases. 1546 | * 1547 | * @param boolean $include_total_count To include the total count of records in the response, use true. 1548 | * @param string $after_cursor Return results after the given pagination cursor. 1549 | * @param string $before_cursor Return results before the given pagination cursor. 1550 | * @param integer $per_page Number of results to return. 1551 | * 1552 | * @since 1.0.0 1553 | * 1554 | * @see https://developers.convertkit.com/v4.html#list-purchases 1555 | * 1556 | * @return false|mixed 1557 | */ 1558 | public function get_purchases( 1559 | bool $include_total_count = false, 1560 | string $after_cursor = '', 1561 | string $before_cursor = '', 1562 | int $per_page = 100 1563 | ) { 1564 | // Send request. 1565 | return $this->get( 1566 | 'purchases', 1567 | $this->build_total_count_and_pagination_params( 1568 | [], 1569 | $include_total_count, 1570 | $after_cursor, 1571 | $before_cursor, 1572 | $per_page 1573 | ) 1574 | ); 1575 | } 1576 | 1577 | /** 1578 | * Retuns a specific purchase. 1579 | * 1580 | * @param integer $purchase_id Purchase ID. 1581 | * 1582 | * @see https://developers.convertkit.com/v4.html#get-a-purchase 1583 | * 1584 | * @return mixed|object 1585 | */ 1586 | public function get_purchase(int $purchase_id) 1587 | { 1588 | return $this->get(sprintf('purchases/%s', $purchase_id)); 1589 | } 1590 | 1591 | /** 1592 | * Creates a purchase. 1593 | * 1594 | * @param string $email_address Email Address. 1595 | * @param string $transaction_id Transaction ID. 1596 | * @param array $products Products. 1597 | * @param string $currency ISO Currency Code. 1598 | * @param string|null $first_name First Name. 1599 | * @param string|null $status Order Status. 1600 | * @param float $subtotal Subtotal. 1601 | * @param float $tax Tax. 1602 | * @param float $shipping Shipping. 1603 | * @param float $discount Discount. 1604 | * @param float $total Total. 1605 | * @param \DateTime|null $transaction_time Transaction date and time. 1606 | * 1607 | * @see https://developers.convertkit.com/v4.html#create-a-purchase 1608 | * 1609 | * @return mixed|object 1610 | */ 1611 | public function create_purchase( 1612 | string $email_address, 1613 | string $transaction_id, 1614 | array $products, 1615 | string $currency = 'USD', 1616 | string|null $first_name = null, 1617 | string|null $status = null, 1618 | float $subtotal = 0, 1619 | float $tax = 0, 1620 | float $shipping = 0, 1621 | float $discount = 0, 1622 | float $total = 0, 1623 | \DateTime|null $transaction_time = null 1624 | ) { 1625 | // Build parameters. 1626 | $options = [ 1627 | // Required fields. 1628 | 'email_address' => $email_address, 1629 | 'transaction_id' => $transaction_id, 1630 | 'products' => $products, 1631 | 'currency' => $currency, // Required, but if not provided, API will default to USD. 1632 | 1633 | // Optional fields. 1634 | 'first_name' => $first_name, 1635 | 'status' => $status, 1636 | 'subtotal' => $subtotal, 1637 | 'tax' => $tax, 1638 | 'shipping' => $shipping, 1639 | 'discount' => $discount, 1640 | 'total' => $total, 1641 | 'transaction_time' => (!is_null($transaction_time) ? $transaction_time->format('Y-m-d H:i:s') : ''), 1642 | ]; 1643 | 1644 | // Iterate through options, removing blank and null entries. 1645 | foreach ($options as $key => $value) { 1646 | if (is_null($value)) { 1647 | unset($options[$key]); 1648 | continue; 1649 | } 1650 | 1651 | if (is_string($value) && strlen($value) === 0) { 1652 | unset($options[$key]); 1653 | } 1654 | } 1655 | 1656 | return $this->post('purchases', $options); 1657 | } 1658 | 1659 | /** 1660 | * List segments. 1661 | * 1662 | * @param boolean $include_total_count To include the total count of records in the response, use true. 1663 | * @param string $after_cursor Return results after the given pagination cursor. 1664 | * @param string $before_cursor Return results before the given pagination cursor. 1665 | * @param integer $per_page Number of results to return. 1666 | * 1667 | * @since 2.0.0 1668 | * 1669 | * @see https://developers.convertkit.com/v4.html#convertkit-api-segments 1670 | * 1671 | * @return false|mixed 1672 | */ 1673 | public function get_segments( 1674 | bool $include_total_count = false, 1675 | string $after_cursor = '', 1676 | string $before_cursor = '', 1677 | int $per_page = 100 1678 | ) { 1679 | // Send request. 1680 | return $this->get( 1681 | 'segments', 1682 | $this->build_total_count_and_pagination_params( 1683 | [], 1684 | $include_total_count, 1685 | $after_cursor, 1686 | $before_cursor, 1687 | $per_page 1688 | ) 1689 | ); 1690 | } 1691 | 1692 | /** 1693 | * Converts any relative URls to absolute, fully qualified HTTP(s) URLs for the given 1694 | * DOM Elements. 1695 | * 1696 | * @param \DOMNodeList<\DOMElement> $elements Elements. 1697 | * @param string $attribute HTML Attribute. 1698 | * @param string $url Absolute URL to prepend to relative URLs. 1699 | * 1700 | * @since 1.0.0 1701 | * 1702 | * @return void 1703 | */ 1704 | public function convert_relative_to_absolute_urls(\DOMNodeList $elements, string $attribute, string $url) // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint, Generic.Files.LineLength.TooLong 1705 | { 1706 | // Anchor hrefs. 1707 | foreach ($elements as $element) { 1708 | // Skip if the attribute's value is empty. 1709 | if (empty($element->getAttribute($attribute))) { 1710 | continue; 1711 | } 1712 | 1713 | // Skip if the attribute's value is a fully qualified URL. 1714 | if (filter_var($element->getAttribute($attribute), FILTER_VALIDATE_URL)) { 1715 | continue; 1716 | } 1717 | 1718 | // Skip if this is a Google Font CSS URL. 1719 | if (strpos($element->getAttribute($attribute), '//fonts.googleapis.com') !== false) { 1720 | continue; 1721 | } 1722 | 1723 | // If here, the attribute's value is a relative URL, missing the http(s) and domain. 1724 | // Prepend the URL to the attribute's value. 1725 | $element->setAttribute($attribute, $url . $element->getAttribute($attribute)); 1726 | } 1727 | } 1728 | 1729 | /** 1730 | * Strips , and opening and closing tags from the given markup, 1731 | * as well as the Content-Type meta tag we might have added in get_html(). 1732 | * 1733 | * @param string $markup HTML Markup. 1734 | * 1735 | * @since 1.0.0 1736 | * 1737 | * @return string HTML Markup 1738 | */ 1739 | public function strip_html_head_body_tags(string $markup) 1740 | { 1741 | $markup = str_replace('', '', $markup); 1742 | $markup = str_replace('', '', $markup); 1743 | $markup = str_replace('', '', $markup); 1744 | $markup = str_replace('', '', $markup); 1745 | $markup = str_replace('', '', $markup); 1746 | $markup = str_replace('', '', $markup); 1747 | $markup = str_replace('', '', $markup); 1748 | 1749 | return $markup; 1750 | } 1751 | 1752 | /** 1753 | * Adds total count and pagination parameters to the given array of existing API parameters. 1754 | * 1755 | * @param array $params API parameters. 1756 | * @param boolean $include_total_count Return total count of records. 1757 | * @param string $after_cursor Return results after the given pagination cursor. 1758 | * @param string $before_cursor Return results before the given pagination cursor. 1759 | * @param integer $per_page Number of results to return. 1760 | * 1761 | * @since 2.0.0 1762 | * 1763 | * @return array 1764 | */ 1765 | private function build_total_count_and_pagination_params( 1766 | array $params = [], 1767 | bool $include_total_count = false, 1768 | string $after_cursor = '', 1769 | string $before_cursor = '', 1770 | int $per_page = 100 1771 | ) { 1772 | $params['include_total_count'] = $include_total_count; 1773 | if (!empty($after_cursor)) { 1774 | $params['after'] = $after_cursor; 1775 | } 1776 | if (!empty($before_cursor)) { 1777 | $params['before'] = $before_cursor; 1778 | } 1779 | if (!empty($per_page)) { 1780 | $params['per_page'] = $per_page; 1781 | } 1782 | 1783 | return $params; 1784 | } 1785 | 1786 | /** 1787 | * Performs a GET request to the API. 1788 | * 1789 | * @param string $endpoint API Endpoint. 1790 | * @param array|string> $args Request arguments. 1791 | * 1792 | * @return false|mixed 1793 | */ 1794 | public function get(string $endpoint, array $args = []) 1795 | { 1796 | return $this->request($endpoint, 'GET', $args); 1797 | } 1798 | 1799 | /** 1800 | * Performs a POST request to the API. 1801 | * 1802 | * @param string $endpoint API Endpoint. 1803 | * @param array>> $args Request arguments. 1804 | * 1805 | * @return false|mixed 1806 | */ 1807 | public function post(string $endpoint, array $args = []) 1808 | { 1809 | return $this->request($endpoint, 'POST', $args); 1810 | } 1811 | 1812 | /** 1813 | * Performs a PUT request to the API. 1814 | * 1815 | * @param string $endpoint API Endpoint. 1816 | * @param array|string> $args Request arguments. 1817 | * 1818 | * @return false|mixed 1819 | */ 1820 | public function put(string $endpoint, array $args = []) 1821 | { 1822 | return $this->request($endpoint, 'PUT', $args); 1823 | } 1824 | 1825 | /** 1826 | * Performs a DELETE request to the API. 1827 | * 1828 | * @param string $endpoint API Endpoint. 1829 | * @param array|string> $args Request arguments. 1830 | * 1831 | * @return false|mixed 1832 | */ 1833 | public function delete(string $endpoint, array $args = []) 1834 | { 1835 | return $this->request($endpoint, 'DELETE', $args); 1836 | } 1837 | 1838 | /** 1839 | * Performs an API request. 1840 | * 1841 | * @param string $endpoint API Endpoint. 1842 | * @param string $method Request method. 1843 | * @param array>> $args Request arguments. 1844 | * 1845 | * @throws \Exception If JSON encoding arguments failed. 1846 | * 1847 | * @return false|mixed 1848 | */ 1849 | abstract public function request(string $endpoint, string $method, array $args = []); 1850 | 1851 | /** 1852 | * Returns the headers to use in an API request. 1853 | * 1854 | * @param string $type Accept and Content-Type Headers. 1855 | * @param boolean $auth Include authorization header. 1856 | * 1857 | * @since 2.0.0 1858 | * 1859 | * @return array 1860 | */ 1861 | abstract public function get_request_headers(string $type = 'application/json', bool $auth = true); 1862 | 1863 | /** 1864 | * Returns the maximum amount of time to wait for 1865 | * a response to the request before exiting. 1866 | * 1867 | * @since 2.0.0 1868 | * 1869 | * @return integer Timeout, in seconds. 1870 | */ 1871 | abstract public function get_timeout(); 1872 | 1873 | /** 1874 | * Returns the user agent string to use in all HTTP requests. 1875 | * 1876 | * @since 2.0.0 1877 | * 1878 | * @return string 1879 | */ 1880 | abstract public function get_user_agent(); 1881 | } 1882 | --------------------------------------------------------------------------------