diff --git a/.env.dist.testing b/.env.dist.testing
index ca3522d..f61980a 100644
--- a/.env.dist.testing
+++ b/.env.dist.testing
@@ -1,4 +1,5 @@
CONVERTKIT_API_BROADCAST_ID="8697158"
+CONVERTKIT_API_CUSTOM_FIELD_ID="264073"
CONVERTKIT_API_FORM_ID="2765139"
CONVERTKIT_API_FORM_ID_2="2780977"
CONVERTKIT_API_LEGACY_FORM_URL="https://app.convertkit.com/landing_pages/470099"
diff --git a/.env.example b/.env.example
index 2628529..a995c61 100644
--- a/.env.example
+++ b/.env.example
@@ -11,6 +11,7 @@ CONVERTKIT_OAUTH_CLIENT_ID=
CONVERTKIT_OAUTH_CLIENT_SECRET=
CONVERTKIT_OAUTH_REDIRECT_URI="https://convertkit-github.local/wp-admin/options-general.php?page=_wp_convertkit_settings"
CONVERTKIT_API_BROADCAST_ID="8697158"
+CONVERTKIT_API_CUSTOM_FIELD_ID="264073"
CONVERTKIT_API_FORM_ID="2765139"
CONVERTKIT_API_FORM_ID_2="2780977"
CONVERTKIT_API_LEGACY_FORM_URL="https://app.convertkit.com/landing_pages/470099"
diff --git a/phpcs.xml b/phpcs.xml
index 9de2bbd..3b059b2 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -72,7 +72,7 @@
-
+
diff --git a/src/ConvertKit_API.php b/src/ConvertKit_API.php
index 9a8e1b9..a44401b 100644
--- a/src/ConvertKit_API.php
+++ b/src/ConvertKit_API.php
@@ -348,13 +348,13 @@ public function get_resource(string $url)
/**
* Performs an API request using Guzzle.
*
- * @param string $endpoint API Endpoint.
- * @param string $method Request method.
- * @param array>> $args Request arguments.
+ * @param string $endpoint API Endpoint.
+ * @param string $method Request method.
+ * @param array>> $args Request arguments.
*
* @throws \Exception If JSON encoding arguments failed.
*
- * @return mixed|object
+ * @return false|mixed
*/
public function request(string $endpoint, string $method, array $args = [])
{
diff --git a/src/ConvertKit_API_Traits.php b/src/ConvertKit_API_Traits.php
index 4511a73..efc67ab 100644
--- a/src/ConvertKit_API_Traits.php
+++ b/src/ConvertKit_API_Traits.php
@@ -945,6 +945,74 @@ public function create_subscribers(array $subscribers, string $callback_url = ''
);
}
+ /**
+ * Filter subscribers based on engagement.
+ *
+ * @param array> $all Array of filter conditions where ALL must be met (AND logic). Each condition can have.
+ * - 'type' (string).
+ * - 'count_greater_than' (int|null).
+ * - 'count_less_than' (int|null).
+ * - 'after' (\DateTime|null).
+ * - 'before' (\DateTime|null).
+ * - 'any' (array|null).
+ * @param boolean $include_total_count To include the total count of records in the response, use true.
+ * @param string $after_cursor Return results after the given pagination cursor.
+ * @param string $before_cursor Return results before the given pagination cursor.
+ * @param integer $per_page Number of results to return.
+ *
+ * @since 2.4.0
+ *
+ * @see https://developers.kit.com/api-reference/subscribers/filter-subscribers-based-on-engagement
+ *
+ * @return mixed
+ */
+ public function filter_subscribers(
+ array $all = [],
+ bool $include_total_count = false,
+ string $after_cursor = '',
+ string $before_cursor = '',
+ int $per_page = 100
+ ) {
+ $options = [];
+
+ foreach ($all as $condition) {
+ $option = [];
+
+ if (array_key_exists('count_greater_than', $condition) && $condition['count_greater_than'] !== null) {
+ $option['count_greater_than'] = $condition['count_greater_than'];
+ }
+
+ if (array_key_exists('count_less_than', $condition) && $condition['count_less_than'] !== null) {
+ $option['count_less_than'] = $condition['count_less_than'];
+ }
+
+ if (array_key_exists('after', $condition) && $condition['after'] instanceof \DateTime) {
+ $option['after'] = $condition['after']->format('Y-m-d');
+ }
+
+ if (array_key_exists('before', $condition) && $condition['before'] instanceof \DateTime) {
+ $option['before'] = $condition['before']->format('Y-m-d');
+ }
+
+ if (array_key_exists('any', $condition) && !empty($condition['any'])) {
+ $option['any'] = (array) $condition['any'];
+ }
+
+ $options[] = $option;
+ }//end foreach
+
+ return $this->post(
+ 'subscribers/filter',
+ $this->build_total_count_and_pagination_params(
+ ['all' => $options],
+ $include_total_count,
+ $after_cursor,
+ $before_cursor,
+ $per_page
+ )
+ );
+ }
+
/**
* Get the ConvertKit subscriber ID associated with email address if it exists.
* Return false if subscriber not found.
@@ -1468,6 +1536,8 @@ public function create_webhook(string $url, string $event, string $parameter = '
case 'subscriber.subscriber_bounce':
case 'subscriber.subscriber_complain':
case 'purchase.purchase_create':
+ case 'custom_field.field_created':
+ case 'custom_field.field_deleted':
$eventData = ['name' => $event];
break;
@@ -1508,6 +1578,13 @@ public function create_webhook(string $url, string $event, string $parameter = '
];
break;
+ case 'custom_field.field_value_updated':
+ $eventData = [
+ 'name' => $event,
+ 'custom_field_id' => $parameter,
+ ];
+ break;
+
default:
throw new \InvalidArgumentException(sprintf('The event %s is not supported', $event));
}//end switch
@@ -1617,6 +1694,36 @@ public function create_custom_fields(array $labels, string $callback_url = '')
);
}
+ /**
+ * Bulk update subscriber custom field values
+ *
+ * @param array> $custom_field_values Array of custom field values to update.
+ * - 'subscriber_id' (int) Subscriber ID.
+ * - 'subscriber_custom_field_id' (int) Custom Field ID.
+ * - 'value' (string|integer) Value to update.
+ * @param string $callback_url URL to notify for large batch size when async processing complete.
+ *
+ * @since 2.4.0
+ *
+ * @see https://developers.kit.com/api-reference/custom-fields/bulk-update-subscriber-custom-field-values
+ *
+ * @return mixed|object
+ */
+ public function update_subscriber_custom_field_values(array $custom_field_values, string $callback_url = '')
+ {
+ // Build parameters.
+ $options = ['custom_field_values' => $custom_field_values];
+ if (!empty($callback_url)) {
+ $options['callback_url'] = $callback_url;
+ }
+
+ // Send request.
+ return $this->post(
+ 'bulk/custom_fields/subscribers',
+ $options
+ );
+ }
+
/**
* Update a custom field.
*
@@ -1854,15 +1961,15 @@ public function strip_html_head_body_tags(string $markup)
/**
* Adds total count and pagination parameters to the given array of existing API parameters.
*
- * @param array $params API parameters.
- * @param boolean $include_total_count Return total count of records.
- * @param string $after_cursor Return results after the given pagination cursor.
- * @param string $before_cursor Return results before the given pagination cursor.
- * @param integer $per_page Number of results to return.
+ * @param array>> $params API parameters.
+ * @param boolean $include_total_count Return total count of records.
+ * @param string $after_cursor Return results after the given pagination cursor.
+ * @param string $before_cursor Return results before the given pagination cursor.
+ * @param integer $per_page Number of results to return.
*
* @since 2.0.0
*
- * @return array
+ * @return array>>
*/
private function build_total_count_and_pagination_params(
array $params = [],
@@ -1888,8 +1995,8 @@ private function build_total_count_and_pagination_params(
/**
* Performs a GET request to the API.
*
- * @param string $endpoint API Endpoint.
- * @param array|string> $args Request arguments.
+ * @param string $endpoint API Endpoint.
+ * @param array|list>> $args Request arguments.
*
* @return false|mixed
*/
@@ -1901,8 +2008,8 @@ public function get(string $endpoint, array $args = [])
/**
* Performs a POST request to the API.
*
- * @param string $endpoint API Endpoint.
- * @param array>> $args Request arguments.
+ * @param string $endpoint API Endpoint.
+ * @param array|boolean|integer|float|string>> $args Request arguments.
*
* @return false|mixed
*/
@@ -1940,9 +2047,9 @@ public function delete(string $endpoint, array $args = [])
/**
* Performs an API request.
*
- * @param string $endpoint API Endpoint.
- * @param string $method Request method.
- * @param array>> $args Request arguments.
+ * @param string $endpoint API Endpoint.
+ * @param string $method Request method.
+ * @param array>> $args Request arguments.
*
* @throws \Exception If JSON encoding arguments failed.
*
diff --git a/tests/ConvertKitAPIKeyTest.php b/tests/ConvertKitAPIKeyTest.php
index 43dca9e..28ec468 100644
--- a/tests/ConvertKitAPIKeyTest.php
+++ b/tests/ConvertKitAPIKeyTest.php
@@ -475,6 +475,33 @@ public function testCreateCustomFields()
$result = $this->api->create_custom_fields($labels);
}
+ /**
+ * Test that update_subscriber_custom_field_values() throws a ClientException
+ * as this is only supported using OAuth.
+ *
+ * @since 2.4.0
+ *
+ * @return void
+ */
+ public function testUpdateSubscriberCustomFieldValues()
+ {
+ $this->expectException(ClientException::class);
+ $result = $this->api->update_subscriber_custom_field_values(
+ [
+ [
+ 'subscriber_id' => 1,
+ 'subscriber_custom_field_id' => (int) $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID'],
+ 'value' => '100',
+ ],
+ [
+ 'subscriber_id' => 2,
+ 'subscriber_custom_field_id' => (int) $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID'],
+ 'value' => '200',
+ ],
+ ]
+ );
+ }
+
/**
* Test that get_purchases() throws a ClientException
* as this is only supported using OAuth.
diff --git a/tests/ConvertKitAPITest.php b/tests/ConvertKitAPITest.php
index 70bbf12..6c48680 100644
--- a/tests/ConvertKitAPITest.php
+++ b/tests/ConvertKitAPITest.php
@@ -3679,6 +3679,166 @@ public function testCreateSubscribersWithInvalidEmailAddresses()
$this->assertCount(2, $result->failures);
}
+ /**
+ * Test that filter_subscribers() returns the expected data.
+ *
+ * @since 2.4.0
+ *
+ * @return void
+ */
+ public function testFilterSubscribers()
+ {
+ $result = $this->api->filter_subscribers(
+ [
+ [
+ 'type' => 'opens',
+ 'count_greater_than' => 10,
+ 'count_less_than' => 100,
+ 'after' => new \DateTime('2024-01-01'),
+ 'before' => new \DateTime('2027-01-01'),
+ ]
+ ]
+ );
+
+ // Assert subscribers and pagination exist.
+ $this->assertDataExists($result, 'subscribers');
+ $this->assertPaginationExists($result);
+ }
+
+ /**
+ * Test that filter_subscribers() returns the expected data
+ * when multiple all conditions are specified.
+ *
+ * @since 2.4.0
+ *
+ * @return void
+ */
+ public function testFilterSubscribersWithMultipleConditions()
+ {
+ $result = $this->api->filter_subscribers(
+ [
+ [
+ 'type' => 'opens',
+ 'count_greater_than' => 10,
+ 'count_less_than' => 100,
+ 'after' => new \DateTime('2024-01-01'),
+ 'before' => new \DateTime('2027-01-01'),
+ ],
+ [
+ 'type' => 'clicks',
+ 'count_greater_than' => 1,
+ 'count_less_than' => 100,
+ 'after' => new \DateTime('2024-01-01'),
+ 'before' => new \DateTime('2027-01-01'),
+ ]
+ ]
+ );
+
+ // Assert subscribers and pagination exist.
+ $this->assertDataExists($result, 'subscribers');
+ $this->assertPaginationExists($result);
+ }
+
+ /**
+ * Test that filter_subscribers() returns the expected data
+ * when multiple any conditions are specified.
+ *
+ * @since 2.4.0
+ *
+ * @return void
+ */
+ public function testFilterSubscribersWithNoParameters()
+ {
+ $result = $this->api->filter_subscribers();
+
+ // Assert subscribers and pagination exist.
+ $this->assertDataExists($result, 'subscribers');
+ $this->assertPaginationExists($result);
+ }
+
+ /**
+ * Test that filter_subscribers() returns the expected data
+ * when the total count is included.
+ *
+ * @since 2.4.0
+ *
+ * @return void
+ */
+ public function testFilterSubscribersWithTotalCount()
+ {
+ $result = $this->api->filter_subscribers(
+ include_total_count: true
+ );
+
+ // Assert subscribers and pagination exist.
+ $this->assertDataExists($result, 'subscribers');
+ $this->assertPaginationExists($result);
+
+ // Assert total count is included.
+ $this->assertArrayHasKey('total_count', get_object_vars($result->pagination));
+ $this->assertGreaterThan(0, $result->pagination->total_count);
+ }
+
+ /**
+ * Test that filter_subscribers() returns the expected data
+ * when pagination parameters and per_page limits are specified.
+ *
+ * @since 2.4.0
+ *
+ * @return void
+ */
+ public function testFilterSubscribersPagination()
+ {
+ $result = $this->api->filter_subscribers(
+ per_page: 1
+ );
+
+ // Assert subscribers and pagination exist.
+ $this->assertDataExists($result, 'subscribers');
+ $this->assertPaginationExists($result);
+
+ // Assert a single subscriber was returned.
+ $this->assertCount(1, $result->subscribers);
+
+ // Assert has_previous_page and has_next_page are correct.
+ $this->assertFalse($result->pagination->has_previous_page);
+ $this->assertTrue($result->pagination->has_next_page);
+
+ // Use pagination to fetch next page.
+ $result = $this->api->filter_subscribers(
+ per_page: 1,
+ after_cursor: $result->pagination->end_cursor
+ );
+
+ // Assert subscribers and pagination exist.
+ $this->assertDataExists($result, 'subscribers');
+ $this->assertPaginationExists($result);
+
+ // Assert a single subscriber was returned.
+ $this->assertCount(1, $result->subscribers);
+
+ // Assert has_previous_page and has_next_page are correct.
+ $this->assertTrue($result->pagination->has_previous_page);
+ $this->assertTrue($result->pagination->has_next_page);
+
+ // Use pagination to fetch previous page.
+ $result = $this->api->filter_subscribers(
+ per_page: 1,
+ before_cursor: $result->pagination->start_cursor
+ );
+
+ // Assert subscribers and pagination exist.
+ $this->assertDataExists($result, 'subscribers');
+ $this->assertPaginationExists($result);
+
+ // Assert a single subscriber was returned.
+ $this->assertCount(1, $result->subscribers);
+
+ // Assert has_previous_page and has_next_page are correct.
+ $this->assertTrue($result->pagination->has_previous_page);
+ $this->assertFalse($result->pagination->has_next_page);
+ }
+
/**
* Test that get_subscriber_id() returns the expected data.
*
@@ -4693,17 +4853,19 @@ public function testCreateWebhookWithEventParameter()
$url = 'https://webhook.site/' . str_shuffle('wfervdrtgsdewrafvwefds');
$result = $this->api->create_webhook(
url: $url,
- event: 'subscriber.form_subscribe',
- parameter: $_ENV['CONVERTKIT_API_FORM_ID']
+ event: 'custom_field.field_value_updated',
+ parameter: $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID']
);
// Confirm webhook created with correct data.
$this->assertArrayHasKey('webhook', get_object_vars($result));
$this->assertArrayHasKey('id', get_object_vars($result->webhook));
+ $this->assertArrayHasKey('account_id', get_object_vars($result->webhook));
+ $this->assertArrayHasKey('event', get_object_vars($result->webhook));
$this->assertArrayHasKey('target_url', get_object_vars($result->webhook));
$this->assertEquals($result->webhook->target_url, $url);
- $this->assertEquals($result->webhook->event->name, 'form_subscribe');
- $this->assertEquals($result->webhook->event->form_id, $_ENV['CONVERTKIT_API_FORM_ID']);
+ $this->assertEquals($result->webhook->event->name, 'field_value_updated');
+ $this->assertEquals($result->webhook->event->custom_field_id, $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID']);
// Delete the webhook.
$result = $this->api->delete_webhook($result->webhook->id);
@@ -4899,6 +5061,55 @@ public function testCreateCustomFields()
$this->assertIsArray($result->custom_fields);
}
+ /**
+ * Test that update_subscriber_custom_field_values() works.
+ *
+ * @since 2.4.0
+ *
+ * @return void
+ */
+ public function testUpdateSubscriberCustomFieldValues()
+ {
+ // Create subscribers.
+ $subscribers = [
+ [
+ 'email_address' => str_replace('@kit.com', '-1@kit.com', $this->generateEmailAddress()),
+ ],
+ [
+ 'email_address' => str_replace('@kit.com', '-2@kit.com', $this->generateEmailAddress()),
+ ],
+ ];
+ $result = $this->api->create_subscribers($subscribers);
+
+ // Set subscriber_id to ensure subscriber is unsubscribed after test.
+ foreach ($result->subscribers as $i => $subscriber) {
+ $this->subscriber_ids[] = $subscriber->id;
+ }
+
+ // Bulk update subscriber custom field values.
+ $result = $this->api->update_subscriber_custom_field_values(
+ [
+ [
+ 'subscriber_id' => $this->subscriber_ids[0],
+ 'subscriber_custom_field_id' => (int) $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID'],
+ 'value' => '100',
+ ],
+ [
+ 'subscriber_id' => $this->subscriber_ids[1],
+ 'subscriber_custom_field_id' => (int) $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID'],
+ 'value' => '200',
+ ],
+ ]
+ );
+
+ // Assert no failures.
+ $this->assertCount(0, $result->failures);
+
+ // Confirm result is an array comprising of each custom field value that was updated.
+ $this->assertIsArray($result->custom_field_values);
+ $this->assertCount(2, $result->custom_field_values);
+ }
+
/**
* Test that update_custom_field() works.
*