From f1e24199efe262ce8e072dfdfae057798538d31f Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 12:29:39 +0800 Subject: [PATCH 01/16] Add Snippet Methods --- src/ConvertKit_API_Traits.php | 159 ++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/src/ConvertKit_API_Traits.php b/src/ConvertKit_API_Traits.php index 1405d22..b08e653 100644 --- a/src/ConvertKit_API_Traits.php +++ b/src/ConvertKit_API_Traits.php @@ -662,6 +662,165 @@ public function get_sequence_subscriptions( ); } + /** + * List snippets + * + * @param boolean $archived When `true`, returns only archived snippets. Defaults to `false`. + * @param boolean $include_content When `true`, includes both the content and document fields for each snippet in the response. Defaults to `false`. + * @param string $snippet_type Filter snippets by type. Use inline for text snippets or block for rich-text block snippets. + * @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. + * + * @see https://developers.kit.com/api-reference/snippets/list-snippets + * + * @return false|mixed + */ + public function get_snippets( + bool $archived = false, + bool $include_content = false, + string $snippet_type = null, + bool $include_total_count = false, + string $after_cursor = '', + string $before_cursor = '', + int $per_page = 100 + ) { + return $this->get( + 'snippets', + $this->build_total_count_and_pagination_params( + [ + 'archived' => $archived, + 'include_content' => $include_content, + 'snippet_type' => $snippet_type, + ], + $include_total_count, + $after_cursor, + $before_cursor, + $per_page + ) + ); + } + + /** + * Create a snippet + * + * @param string $name Name of the snippet. + * @param string $snippet_type Type of snippet. Must be one of: `inline`, `block`. + * @param string $content Content of the snippet. + * + * @see https://developers.kit.com/api-reference/snippets/create-a-snippet + * + * @return mixed|object + */ + public function create_snippet( + string $name, + string $snippet_type, + string $content + ) { + $options = [ + 'name' => $name, + 'snippet_type' => $snippet_type, + ]; + + switch ($snippet_type) { + case 'inline': + $options['content'] = $content; + break; + + case 'block': + default: + $options['document_attributes'] = ['value_html' => $content]; + break; + } + + // Send request. + return $this->post( + 'snippets', + $options + ); + } + + /** + * Get a snippet. + * + * @param integer $id Snippet ID. + * + * @see https://developers.kit.com/api-reference/snippets/get-a-snippet + * + * @return mixed|object + */ + public function get_snippet(int $id) + { + return $this->get(sprintf('snippets/%s', $id)); + } + + /** + * Updates a snippet + * + * @param integer $snippet_id Snippet ID. + * @param string $name Name of the snippet. + * @param string $snippet_type Type of snippet. Must be one of: `inline`, `block`. + * @param boolean $archived Pass `true` to archive or `false` to restore the snippet. + * @param string $content Content of the snippet. + * + * @see https://developers.kit.com/api-reference/snippets/update-a-snippet + * + * @return mixed|object + */ + public function update_snippet( + int $snippet_id, + string $name = '', + string $snippet_type = '', + bool $archived = false, + string $content = '' + ) { + $options = [ + 'name' => $name, + 'snippet_type' => $snippet_type, + 'archived' => $archived, + ]; + + switch ($snippet_type) { + case 'inline': + $options['content'] = $content; + break; + + case 'block': + default: + $options['document_attributes'] = ['value_html' => $content]; + break; + } + + // Iterate through options, removing blank entries. + foreach ($options as $key => $value) { + if (is_string($value) && strlen($value) === 0) { + unset($options[$key]); + } + } + + // Send request. + return $this->put( + sprintf('snippets/%s', $snippet_id), + $options + ); + } + + /** + * Deletes a snippet. + * + * @param integer $id Snippet ID. + * + * @see https://developers.kit.com/api-reference/snippets/delete-a-snippet + * + * @return mixed|object + */ + public function delete_snippet(int $id) + { + return $this->delete(sprintf('snippets/%s', $id)); + } + + /** * List tags. * From 9d78a79dde693bd282b8982bf6da66c03f00a08a Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 13:11:40 +0800 Subject: [PATCH 02/16] Remove delete_snippets(), as the API has no delete snippet endpoint --- src/ConvertKit_API_Traits.php | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/src/ConvertKit_API_Traits.php b/src/ConvertKit_API_Traits.php index b08e653..42dd987 100644 --- a/src/ConvertKit_API_Traits.php +++ b/src/ConvertKit_API_Traits.php @@ -665,13 +665,13 @@ public function get_sequence_subscriptions( /** * List snippets * - * @param boolean $archived When `true`, returns only archived snippets. Defaults to `false`. - * @param boolean $include_content When `true`, includes both the content and document fields for each snippet in the response. Defaults to `false`. - * @param string $snippet_type Filter snippets by type. Use inline for text snippets or block for rich-text block snippets. - * @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. + * @param boolean $archived When `true`, returns only archived snippets. Defaults to `false`. + * @param boolean $include_content When `true`, includes both the content and document fields for each snippet in the response. Defaults to `false`. + * @param string|null $snippet_type Filter snippets by type. Use inline for text snippets or block for rich-text block snippets. + * @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. * * @see https://developers.kit.com/api-reference/snippets/list-snippets * @@ -680,7 +680,7 @@ public function get_sequence_subscriptions( public function get_snippets( bool $archived = false, bool $include_content = false, - string $snippet_type = null, + string|null $snippet_type = null, bool $include_total_count = false, string $after_cursor = '', string $before_cursor = '', @@ -806,21 +806,6 @@ public function update_snippet( ); } - /** - * Deletes a snippet. - * - * @param integer $id Snippet ID. - * - * @see https://developers.kit.com/api-reference/snippets/delete-a-snippet - * - * @return mixed|object - */ - public function delete_snippet(int $id) - { - return $this->delete(sprintf('snippets/%s', $id)); - } - - /** * List tags. * From f0d74a3628d7e1b0aa38c29ecbbf7de6255fbadc Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 13:11:55 +0800 Subject: [PATCH 03/16] Added Tests --- tests/ConvertKitAPITest.php | 246 ++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) diff --git a/tests/ConvertKitAPITest.php b/tests/ConvertKitAPITest.php index 8263c63..be0af11 100644 --- a/tests/ConvertKitAPITest.php +++ b/tests/ConvertKitAPITest.php @@ -1601,6 +1601,252 @@ public function testGetSequenceSubscriptionsWithInvalidPagination() ); } + /** + * Test that get_snippets() returns the expected data. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSnippets() + { + $result = $this->api->get_snippets(); + + // Assert snippets and pagination exist. + $this->assertDataExists($result, 'snippets'); + $this->assertPaginationExists($result); + + // Check first snippet in resultset has expected data. + $snippet = get_object_vars($result->snippets[0]); + $this->assertArrayHasKey('id', $snippet); + $this->assertArrayHasKey('name', $snippet); + $this->assertArrayHasKey('snippet_type', $snippet); + $this->assertArrayHasKey('archived', $snippet); + $this->assertArrayHasKey('key', $snippet); + $this->assertArrayHasKey('created_at', $snippet); + $this->assertArrayHasKey('updated_at', $snippet); + } + + /** + * Test that get_snippets() returns the expected data when + * the snippet type is inline. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetInlineSnippets() + { + $result = $this->api->get_snippets( + snippet_type: 'inline' + ); + + // Assert snippets and pagination exist. + $this->assertDataExists($result, 'snippets'); + $this->assertPaginationExists($result); + + // Assert two snippets were returned. + $this->assertCount(2, $result->snippets); + } + + /** + * Test that get_snippets() returns the expected data when + * the snippet type is block. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetBlockSnippets() + { + $result = $this->api->get_snippets( + snippet_type: 'block' + ); + + // Assert snippets and pagination exist. + $this->assertDataExists($result, 'snippets'); + $this->assertPaginationExists($result); + + // Assert no snippets were returned. + $this->assertCount(0, $result->snippets); + } + + /** + * Test that get_snippets() returns the expected data + * when the total count is included. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSnippetsWithTotalCount() + { + $result = $this->api->get_snippets( + include_total_count: true + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'snippets'); + $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 get_snippets() returns the expected data when + * pagination parameters and per_page limits are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSnippetsPagination() + { + $result = $this->api->get_snippets( + per_page: 1 + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'snippets'); + $this->assertPaginationExists($result); + + // Assert a single sequence was returned. + $this->assertCount(1, $result->snippets); + + // 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->get_snippets( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'snippets'); + $this->assertPaginationExists($result); + + // Assert a single sequence was returned. + $this->assertCount(1, $result->snippets); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertFalse($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_snippets( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'snippets'); + $this->assertPaginationExists($result); + + // Assert a single sequence was returned. + $this->assertCount(1, $result->snippets); + } + + /** + * Test that create_snippet() and update_snippet() works. + * + * @since 2.5.0 + * + * @return void + */ + public function testCreateAndUpdateSnippet() + { + // Create a snippet. + $result = $this->api->create_snippet( + name: 'Test Snippet', + snippet_type: 'inline', + content: 'Test Content' + ); + $snippetID = $result->snippet->id; + + // Confirm the Snippet saved. + $result = get_object_vars($result->snippet); + $this->assertArrayHasKey('id', $result); + $this->assertEquals('Test Snippet', $result['name']); + $this->assertEquals('inline', $result['snippet_type']); + $this->assertEquals('Test Content', $result['content']); + + // Update the existing sequence. + $result = $this->api->update_snippet( + snippet_id: $snippetID, + name: 'Edited Test Snippet', + snippet_type: 'inline', + content: 'Edited Test Content' + ); + + // Confirm the changes saved. + $result = get_object_vars($result->snippet); + $this->assertArrayHasKey('id', $result); + $this->assertEquals('Edited Test Snippet', $result['name']); + $this->assertEquals('inline', $result['snippet_type']); + $this->assertEquals('Edited Test Content', $result['content']); + } + + /** + * Test that get_snippet() returns the expected data. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSnippet() + { + $result = $this->api->get_snippet((int) $_ENV['CONVERTKIT_API_SNIPPET_ID']); + $this->assertInstanceOf('stdClass', $result); + $this->assertArrayHasKey('snippet', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->snippet)); + } + + /** + * Test that get_snippet() throws a ClientException when an invalid + * snippet ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSnippetWithInvalidSnippetID() + { + $this->expectException(ClientException::class); + $this->api->get_snippet(12345); + } + + /** + * Test that update_snippet() throws a ClientException when an invalid + * snippet ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testUpdateSnippetWithInvalidSnippetID() + { + $this->expectException(ClientException::class); + $this->api->update_snippet(12345); + } + + /** + * Test that delete_snippet() throws a ClientException when an invalid + * snippet ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testDeleteSnippetWithInvalidSnippetID() + { + $this->expectException(ClientException::class); + $this->api->delete_snippet(12345); + } + /** * Test that get_tags() returns the expected data. * From dbeb21772371afda59b6ad351b01fec4c7304362 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 13:12:00 +0800 Subject: [PATCH 04/16] Updated .env --- .env.dist.testing | 1 + .env.example | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.env.dist.testing b/.env.dist.testing index 54c9ea6..f69b10f 100644 --- a/.env.dist.testing +++ b/.env.dist.testing @@ -14,3 +14,4 @@ CONVERTKIT_API_SUBSCRIBER_EMAIL="optin@n7studios.com" CONVERTKIT_API_SUBSCRIBER_ID="1579118532" CONVERTKIT_API_EMAIL_TEMPLATE_ID="5215567" CONVERTKIT_API_POST_ID="3175837" +CONVERTKIT_API_SNIPPET_ID="136038" diff --git a/.env.example b/.env.example index a995c61..a8ce412 100644 --- a/.env.example +++ b/.env.example @@ -24,3 +24,6 @@ CONVERTKIT_API_TAG_NAME_2="gravityforms-tag-1" CONVERTKIT_API_TAG_ID_2="2907192" CONVERTKIT_API_SUBSCRIBER_EMAIL="optin@n7studios.com" CONVERTKIT_API_SUBSCRIBER_ID="1579118532" +CONVERTKIT_API_EMAIL_TEMPLATE_ID="5215567" +CONVERTKIT_API_POST_ID="3175837" +CONVERTKIT_API_SNIPPET_ID="136038" From a597aeaddabf1a58c2850a803d03bb213457ec28 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 14:14:26 +0800 Subject: [PATCH 05/16] List Subscribers: Add Support for `include` parameter --- src/ConvertKit_API_Traits.php | 5 +++++ tests/ConvertKitAPITest.php | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/ConvertKit_API_Traits.php b/src/ConvertKit_API_Traits.php index 42dd987..ace755a 100644 --- a/src/ConvertKit_API_Traits.php +++ b/src/ConvertKit_API_Traits.php @@ -1167,6 +1167,7 @@ public function get_post(int $id) * @param \DateTime|null $updated_before Filter subscribers who have been updated before this date. * @param string $sort_field Sort Field (id|updated_at|cancelled_at). * @param string $sort_order Sort Order (asc|desc). + * @param array $include Additional fields to include: attribution, tags, location, canceled_at. * @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. @@ -1187,6 +1188,7 @@ public function get_subscribers( \DateTime|null $updated_before = null, string $sort_field = 'id', string $sort_order = 'desc', + array $include = [], bool $include_total_count = false, string $after_cursor = '', string $before_cursor = '', @@ -1219,6 +1221,9 @@ public function get_subscribers( if (!empty($sort_order)) { $options['sort_order'] = $sort_order; } + if (!empty($include)) { + $options['include'] = implode(',', $include); + } // Send request. return $this->get( diff --git a/tests/ConvertKitAPITest.php b/tests/ConvertKitAPITest.php index be0af11..10e72e4 100644 --- a/tests/ConvertKitAPITest.php +++ b/tests/ConvertKitAPITest.php @@ -3690,6 +3690,28 @@ public function testGetSubscribersWithSortOrderParam() ); } + /** + * Test that get_subscribers() returns the expected data + * when the include parameter is used. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSubscribersWithIncludeParam() + { + $result = $this->api->get_subscribers( + include: ['tags'] + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert fields are included. + $this->assertArrayHasKey('tags', get_object_vars($result->subscribers[0])); + } + /** * Test that get_subscribers() returns the expected data * when pagination parameters and per_page limits are specified. From dd1679563e8d5dfcfa015f4f62b00828b77ef7ae Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 15:12:26 +0800 Subject: [PATCH 06/16] Add Sequence Email Methods --- src/ConvertKit_API_Traits.php | 190 ++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/src/ConvertKit_API_Traits.php b/src/ConvertKit_API_Traits.php index ace755a..e13af18 100644 --- a/src/ConvertKit_API_Traits.php +++ b/src/ConvertKit_API_Traits.php @@ -662,6 +662,196 @@ public function get_sequence_subscriptions( ); } + /** + * List sequence emails + * + * @param integer $sequence_id Sequence ID. + * @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. + * + * @see https://developers.kit.com/api-reference/sequence-emails/list-sequence-emails + * + * @return false|mixed + */ + public function get_sequence_emails( + int $sequence_id, + bool $include_total_count = false, + string $after_cursor = '', + string $before_cursor = '', + int $per_page = 100 + ) { + return $this->get( + sprintf('sequences/%s/emails', $sequence_id), + $this->build_total_count_and_pagination_params( + [], + $include_total_count, + $after_cursor, + $before_cursor, + $per_page + ) + ); + } + + /** + * Create a sequence email + * + * @param integer $sequence_id Sequence ID. + * @param string $subject Subject line of the email. + * @param integer $delay_value Number of days or hours to wait before sending this email after the previous one. + * @param string $delay_unit Unit for the send delay. Use `days` for schedule-aware delivery, `hours` for a fixed hourly delay. + * @param string|null $preview_text Preview text shown in email clients before the email is opened. + * @param string|null $content HTML body content of the email. + * @param integer|null $email_template_id ID of the email template to use for layout and styling. + * @param boolean $published Whether the email is active and will be sent to subscribers. + * @param array|null $send_days Days of the week this email may be sent. Defaults to all 7 days (inherits the sequence schedule). Pass a subset to restrict delivery, or null to reset to all days. + * @param integer|null $position Zero-based position of the email in the sequence. Assigned automatically after the last email if omitted. + * + * @see https://developers.kit.com/api-reference/sequence-emails/create-a-sequence-email + * + * @return mixed|object + */ + public function create_sequence_email( + int $sequence_id, + string $subject, + int $delay_value, + string $delay_unit, + string|null $preview_text = null, + string|null $content = null, + int|null $email_template_id = null, + bool $published = false, + array|null $send_days = null, + int|null $position = null, + ) { + $options = [ + 'subject' => $subject, + 'delay_value' => $delay_value, + 'delay_unit' => $delay_unit, + 'published' => $published, + 'send_days' => $send_days, + ]; + + if (!empty($preview_text)) { + $options['preview_text'] = $preview_text; + } + if (!empty($content)) { + $options['content'] = $content; + } + if (!empty($email_template_id)) { + $options['email_template_id'] = $email_template_id; + } + if (!empty($position)) { + $options['position'] = $position; + } + + // Send request. + return $this->post( + sprintf('sequences/%s/emails', $sequence_id), + $options + ); + } + + /** + * Get a sequence email. + * + * @param integer $sequence_id Sequence ID. + * @param integer $email_id Email ID. + * + * @see https://developers.kit.com/api-reference/sequence-emails/get-a-sequence-email + * + * @return mixed|object + */ + public function get_sequence_email(int $sequence_id, int $email_id) + { + return $this->get(sprintf('sequences/%s/emails/%s', $sequence_id, $email_id)); + } + + /** + * Updates a sequence + * + * @param integer $sequence_id Sequence ID. + * @param integer $email_id Sequence Email ID. + * @param string|null $subject Subject line of the email. + * @param integer|null $delay_value Number of days or hours to wait before sending this email after the previous one. + * @param string|null $delay_unit Unit for the send delay. Use `days` for schedule-aware delivery, `hours` for a fixed hourly delay. + * @param string|null $preview_text Preview text shown in email clients before the email is opened. + * @param string|null $content HTML body content of the email. + * @param integer|null $email_template_id ID of the email template to use for layout and styling. + * @param boolean|null $published Whether the email is active and will be sent to subscribers. + * @param array|null $send_days Days of the week this email may be sent. Defaults to all 7 days (inherits the sequence schedule). Pass a subset to restrict delivery, or null to reset to all days. + * @param integer|null $position Zero-based position of the email in the sequence. Assigned automatically after the last email if omitted. + * + * @see https://developers.kit.com/api-reference/sequences/create-a-sequence + * + * @return mixed|object + */ + public function update_sequence_email( + int $sequence_id, + int $email_id, + string|null $subject = null, + int|null $delay_value = null, + string|null $delay_unit = null, + string|null $preview_text = null, + string|null $content = null, + int|null $email_template_id = null, + bool|null $published = null, + array|null $send_days = null, + int|null $position = null, + ) { + // Build parameters. + $options = ['send_days' => $send_days]; + + if (!is_null($subject)) { + $options['subject'] = $subject; + } + if (!is_null($delay_value)) { + $options['delay_value'] = $delay_value; + } + if (!is_null($delay_unit)) { + $options['delay_unit'] = $delay_unit; + } + if (!is_null($preview_text)) { + $options['preview_text'] = $preview_text; + } + if (!is_null($content)) { + $options['content'] = $content; + } + if (!is_null($email_template_id)) { + $options['email_template_id'] = $email_template_id; + } + if (!is_null($published)) { + $options['published'] = $published; + } + if (!is_null($send_days)) { + $options['send_days'] = $send_days; + } + if (!is_null($position)) { + $options['position'] = $position; + } + + // Send request. + return $this->put( + sprintf('sequences/%s/emails/%s', $sequence_id, $email_id), + $options + ); + } + + /** + * Deletes a sequence email. + * + * @param integer $sequence_id Sequence ID. + * @param integer $email_id Email ID. + * + * @see https://developers.kit.com/api-reference/sequence-emails/delete-a-sequence-email + * + * @return mixed|object + */ + public function delete_sequence_email(int $sequence_id, int $email_id) + { + return $this->delete(sprintf('sequences/%s/emails/%s', $sequence_id, $email_id)); + } + /** * List snippets * From ebe0e95f2529cf31d1cbd7a6b32f07625b9a95f9 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 15:25:00 +0800 Subject: [PATCH 07/16] PHPStan compat. --- src/ConvertKit_API_Traits.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/ConvertKit_API_Traits.php b/src/ConvertKit_API_Traits.php index 42dd987..200bc32 100644 --- a/src/ConvertKit_API_Traits.php +++ b/src/ConvertKit_API_Traits.php @@ -686,14 +686,17 @@ public function get_snippets( string $before_cursor = '', int $per_page = 100 ) { + $options = [ + 'archived' => $archived, + 'include_content' => $include_content, + ]; + if (!is_null($snippet_type)) { + $options['snippet_type'] = $snippet_type; + } return $this->get( 'snippets', $this->build_total_count_and_pagination_params( - [ - 'archived' => $archived, - 'include_content' => $include_content, - 'snippet_type' => $snippet_type, - ], + $options, $include_total_count, $after_cursor, $before_cursor, From 38535dfdc4e472feb7f2fcaff9052ffef8a789b9 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 15:42:06 +0800 Subject: [PATCH 08/16] Added tests --- tests/ConvertKitAPITest.php | 297 ++++++++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) diff --git a/tests/ConvertKitAPITest.php b/tests/ConvertKitAPITest.php index 10e72e4..be0df30 100644 --- a/tests/ConvertKitAPITest.php +++ b/tests/ConvertKitAPITest.php @@ -1601,6 +1601,303 @@ public function testGetSequenceSubscriptionsWithInvalidPagination() ); } + /** + * Test that get_sequence_emails() returns the expected data. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmails() + { + $result = $this->api->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'] + ); + + // Assert emails and pagination exist. + $this->assertDataExists($result, 'emails'); + $this->assertPaginationExists($result); + + // Check first sequence in resultset has expected data. + $sequence = get_object_vars($result->sequences[0]); + $this->assertArrayHasKey('id', $sequence); + $this->assertArrayHasKey('sequence_id', $sequence); + $this->assertArrayHasKey('subject', $email); + $this->assertArrayHasKey('preview_text', $email); + $this->assertArrayHasKey('email_address', $email); + $this->assertArrayHasKey('email_template_id', $email); + $this->assertArrayHasKey('published', $email); + $this->assertArrayHasKey('position', $email); + $this->assertArrayHasKey('delay_value', $email); + $this->assertArrayHasKey('delay_unit', $email); + $this->assertArrayHasKey('send_days', $email); + } + + /** + * Test that get_sequence_emails() returns the expected data + * when the total count is included. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmailsWithTotalCount() + { + $result = $this->api->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + include_total_count: true + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'emails'); + $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 get_sequence_emails() returns the expected data when + * pagination parameters and per_page limits are specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmailsPagination() + { + $result = $this->api->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + per_page: 1 + ); + + // Assert emails and pagination exist. + $this->assertDataExists($result, 'emails'); + $this->assertPaginationExists($result); + + // Assert a single email was returned. + $this->assertCount(1, $result->emails); + + // 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->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert emails and pagination exist. + $this->assertDataExists($result, 'emails'); + $this->assertPaginationExists($result); + + // Assert a single email was returned. + $this->assertCount(1, $result->emails); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertFalse($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert emails and pagination exist. + $this->assertDataExists($result, 'emails'); + $this->assertPaginationExists($result); + + // Assert a single email was returned. + $this->assertCount(1, $result->emails); + } + + /** + * Test that get_sequence_emails() throws a ClientException when an invalid + * sequence ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmailsWithInvalidSequenceID() + { + $this->expectException(ClientException::class); + $result = $this->api->get_sequence_emails( + sequence_id: 12345 + ); + } + + /** + * Test that create_sequence_email(), get_sequence_email(), update_sequence_email() + * and delete_sequence_email() works. + * + * We do all tests in a single function, so we don't end up with unnecessary + * Sequence Emails remaining on the Kit account when running tests, which might impact + * other tests that expect (or do not expect) specific Sequence Emails. + * + * @since 2.5.0 + * + * @return void + */ + public function testCreateGetUpdateAndDeleteSequenceEmail() + { + // Create a sequence email. + $result = $this->api->create_sequence_email( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + subject: 'Test Sequence Email', + delay_value: 1, + delay_unit: 'days', + preview_text: 'Test Preview Text', + content: 'Test Content', + email_template_id: (int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], + published: false, + send_days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], + position: 0 + ); + $sequenceEmailID = $result->email->id; + + // Confirm the Sequence Email saved. + $result = get_object_vars($result->email); + $this->assertArrayHasKey('id', $result); + $this->assertEquals('Test Sequence Email', $result['subject']); + $this->assertEquals(1, $result['delay_value']); + $this->assertEquals('days', $result['delay_unit']); + $this->assertEquals('Test Preview Text', $result['preview_text']); + $this->assertEquals('Test Content', $result['content']); + $this->assertEquals((int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], $result['email_template_id']); + $this->assertEquals(false, $result['published']); + $this->assertEquals(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], $result['send_days']); + $this->assertEquals(0, $result['position']); + + // Get the sequence email. + $result = $this->api->get_sequence_email( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + email_id: $sequenceEmailID + ); + var_dump($result); + die(); + + // Update the existing sequence email. + $result = $this->api->update_sequence_email( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + email_id: $sequenceEmailID, + subject: 'Edited Test Sequence Email', + preview_text: 'Edited Test Preview Text', + content: 'Edited Test Content', + delay_value: 2, + delay_unit: 'hours', + email_template_id: (int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], + published: true, + send_days: ['saturday', 'sunday'], + position: 1, + ); + + // Confirm the changes saved. + $result = get_object_vars($result->email); + $this->assertArrayHasKey('id', $result); + $this->assertEquals('Edited Test Sequence Email', $result['subject']); + $this->assertEquals(2, $result['delay_value']); + $this->assertEquals('hours', $result['delay_unit']); + $this->assertEquals('Edited Test Preview Text', $result['preview_text']); + $this->assertEquals('Edited Test Content', $result['content']); + $this->assertEquals((int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], $result['email_template_id']); + $this->assertEquals(true, $result['published']); + $this->assertEquals(['saturday', 'sunday'], $result['send_days']); + $this->assertEquals(1, $result['position']); + + // Delete Sequence Email. + $this->api->delete_sequence_email($sequenceID, $sequenceEmailID); + $this->assertEquals(204, $this->api->getResponseInterface()->getStatusCode()); + } + + /** + * Test that get_sequence_email() returns the expected data. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmailWithInvalidSequenceID() + { + $this->expectException(ClientException::class); + $this->api->get_sequence_email(12345, 12345); + } + + /** + * Test that get_sequence_email() throws a ClientException when an invalid + * email ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmailWithInvalidEmailID() + { + $this->expectException(ClientException::class); + $this->api->get_sequence_email((int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], 12345); + } + + /** + * Test that update_sequence() throws a ClientException when an invalid + * sequence email ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testUpdateSequenceEmailWithInvalidSequenceID() + { + $this->expectException(ClientException::class); + $this->api->update_sequence_email(12345, 12345); + } + + /** + * Test that update_sequence_email() throws a ClientException when an invalid + * email ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testUpdateSequenceEmailWithInvalidEmailID() + { + $this->expectException(ClientException::class); + $this->api->update_sequence_email((int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], 12345); + } + + /** + * Test that delete_sequence_email() throws a ClientException when an invalid + * sequence email ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testDeleteSequenceEmailWithInvalidSequenceID() + { + $this->expectException(ClientException::class); + $this->api->delete_sequence_email(12345, 12345); + } + + /** + * Test that delete_sequence_email() throws a ClientException when an invalid + * email ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testDeleteSequenceEmailWithInvalidEmailID() + { + $this->expectException(ClientException::class); + $this->api->delete_sequence_email((int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], 12345); + } + /** * Test that get_snippets() returns the expected data. * From 8754ed6b04369c817fbe74a6530e10ed04b94d56 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 15:47:22 +0800 Subject: [PATCH 09/16] Fix tests --- tests/ConvertKitAPITest.php | 59 ++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/tests/ConvertKitAPITest.php b/tests/ConvertKitAPITest.php index be0af11..4597175 100644 --- a/tests/ConvertKitAPITest.php +++ b/tests/ConvertKitAPITest.php @@ -1645,8 +1645,8 @@ public function testGetInlineSnippets() $this->assertDataExists($result, 'snippets'); $this->assertPaginationExists($result); - // Assert two snippets were returned. - $this->assertCount(2, $result->snippets); + // Assert snippets were returned. + $this->assertGreaterThan(0, count($result->snippets)); } /** @@ -1671,6 +1671,28 @@ public function testGetBlockSnippets() $this->assertCount(0, $result->snippets); } + /** + * Test that get_snippets() returns the expected data when + * the archived parameter is used. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSnippetsWithArchivedParam() + { + $result = $this->api->get_snippets( + archived: true + ); + + // Assert snippets and pagination exist. + $this->assertDataExists($result, 'snippets'); + $this->assertPaginationExists($result); + + // Assert snippets were returned. + $this->assertGreaterThan(0, count($result->snippets)); + } + /** * Test that get_snippets() returns the expected data * when the total count is included. @@ -1698,7 +1720,7 @@ public function testGetSnippetsWithTotalCount() * Test that get_snippets() returns the expected data when * pagination parameters and per_page limits are specified. * - * @since 2.0.0 + * @since 2.5.0 * * @return void */ @@ -1751,14 +1773,28 @@ public function testGetSnippetsPagination() } /** - * Test that create_snippet() and update_snippet() works. + * Test that create_snippet() works. * * @since 2.5.0 * * @return void */ - public function testCreateAndUpdateSnippet() + public function testCreateSnippet() { + // Add mock handler for this API request, as the API doesn't provide + // a method to delete snippets to cleanup the test. + $this->api = $this->mockResponse( + api: $this->api, + responseBody: [ + 'snippet' => [ + 'id' => 12345, + 'name' => 'Test Snippet', + 'snippet_type' => 'inline', + 'content' => 'Test Content', + ], + ] + ); + // Create a snippet. $result = $this->api->create_snippet( name: 'Test Snippet', @@ -1773,10 +1809,19 @@ public function testCreateAndUpdateSnippet() $this->assertEquals('Test Snippet', $result['name']); $this->assertEquals('inline', $result['snippet_type']); $this->assertEquals('Test Content', $result['content']); + } - // Update the existing sequence. + /** + * Test that update_snippet() works. + * + * @since 2.5.0 + * + * @return void + */ + public function testUpdateSnippet() + { $result = $this->api->update_snippet( - snippet_id: $snippetID, + snippet_id: (int) $_ENV['CONVERTKIT_API_SNIPPET_ID'], name: 'Edited Test Snippet', snippet_type: 'inline', content: 'Edited Test Content' From cc87e840326c3809b386737ebf0c33741fc9a50f Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 15:49:28 +0800 Subject: [PATCH 10/16] Coding standards --- tests/ConvertKitAPITest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ConvertKitAPITest.php b/tests/ConvertKitAPITest.php index 4597175..98abcb7 100644 --- a/tests/ConvertKitAPITest.php +++ b/tests/ConvertKitAPITest.php @@ -1684,7 +1684,7 @@ public function testGetSnippetsWithArchivedParam() $result = $this->api->get_snippets( archived: true ); - + // Assert snippets and pagination exist. $this->assertDataExists($result, 'snippets'); $this->assertPaginationExists($result); From fa75a3057fdab9f047242009d1a792050fafb02a Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 15:57:33 +0800 Subject: [PATCH 11/16] Remove redundant test --- tests/ConvertKitAPITest.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/ConvertKitAPITest.php b/tests/ConvertKitAPITest.php index 98abcb7..09f64fe 100644 --- a/tests/ConvertKitAPITest.php +++ b/tests/ConvertKitAPITest.php @@ -1878,20 +1878,6 @@ public function testUpdateSnippetWithInvalidSnippetID() $this->api->update_snippet(12345); } - /** - * Test that delete_snippet() throws a ClientException when an invalid - * snippet ID is specified. - * - * @since 2.5.0 - * - * @return void - */ - public function testDeleteSnippetWithInvalidSnippetID() - { - $this->expectException(ClientException::class); - $this->api->delete_snippet(12345); - } - /** * Test that get_tags() returns the expected data. * From f16feb46ddb59756db3e810babfaaea59b2362e0 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 16:34:39 +0800 Subject: [PATCH 12/16] Run Tests Sequentially --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d389cee..1c322ab 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,7 @@ jobs: # Defines PHP Versions matrix to run tests on strategy: fail-fast: false + max-parallel: 1 matrix: php-versions: [ '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] From f29083fb2949605fe44b579b1fad9aadb899d5a1 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 17:15:42 +0800 Subject: [PATCH 13/16] PHPStan compat. --- src/ConvertKit_API_Traits.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ConvertKit_API_Traits.php b/src/ConvertKit_API_Traits.php index b24b973..7462457 100644 --- a/src/ConvertKit_API_Traits.php +++ b/src/ConvertKit_API_Traits.php @@ -1170,7 +1170,7 @@ public function get_post(int $id) * @param \DateTime|null $updated_before Filter subscribers who have been updated before this date. * @param string $sort_field Sort Field (id|updated_at|cancelled_at). * @param string $sort_order Sort Order (asc|desc). - * @param array $include Additional fields to include: attribution, tags, location, canceled_at. + * @param array $include Additional fields to include: attribution, tags, location, canceled_at. * @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. From 7063af2f0c94985e65e0f0b22fc60b36032f1e9b Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 20:07:33 +0800 Subject: [PATCH 14/16] Coding standards --- tests/ConvertKitAPITest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/ConvertKitAPITest.php b/tests/ConvertKitAPITest.php index 5bdfd8a..9a0f85f 100644 --- a/tests/ConvertKitAPITest.php +++ b/tests/ConvertKitAPITest.php @@ -1630,7 +1630,7 @@ public function testGetSequenceEmails() $this->assertArrayHasKey('position', $email); $this->assertArrayHasKey('delay_value', $email); $this->assertArrayHasKey('delay_unit', $email); - $this->assertArrayHasKey('send_days', $email); + $this->assertArrayHasKey('send_days', $email); } /** @@ -1816,7 +1816,8 @@ public function testCreateGetUpdateAndDeleteSequenceEmail() } /** - * Test that get_sequence_email() returns the expected data. + * Test that get_sequence_email() throws a ClientException when an invalid + * sequence ID is specified. * * @since 2.5.0 * @@ -1843,7 +1844,7 @@ public function testGetSequenceEmailWithInvalidEmailID() } /** - * Test that update_sequence() throws a ClientException when an invalid + * Test that update_sequence_email() throws a ClientException when an invalid * sequence email ID is specified. * * @since 2.5.0 From c9ccbcd4056a36424d5423251e28d3f346fcbd72 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 19 May 2026 10:09:09 +0800 Subject: [PATCH 15/16] Fix test --- tests/ConvertKitAPITest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ConvertKitAPITest.php b/tests/ConvertKitAPITest.php index 9a0f85f..497bef6 100644 --- a/tests/ConvertKitAPITest.php +++ b/tests/ConvertKitAPITest.php @@ -1619,9 +1619,9 @@ public function testGetSequenceEmails() $this->assertPaginationExists($result); // Check first sequence in resultset has expected data. - $sequence = get_object_vars($result->sequences[0]); - $this->assertArrayHasKey('id', $sequence); - $this->assertArrayHasKey('sequence_id', $sequence); + $email = get_object_vars($result->emails[0]); + $this->assertArrayHasKey('id', $email); + $this->assertArrayHasKey('sequence_id', $email); $this->assertArrayHasKey('subject', $email); $this->assertArrayHasKey('preview_text', $email); $this->assertArrayHasKey('email_address', $email); From 4d23e7b3e024e98bb57e32ce9e31b35e85ee0277 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 19 May 2026 13:59:11 +0800 Subject: [PATCH 16/16] Fix `testCreateGetUpdateAndDeleteSequenceEmail` test --- tests/ConvertKitAPITest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/ConvertKitAPITest.php b/tests/ConvertKitAPITest.php index 497bef6..2620edb 100644 --- a/tests/ConvertKitAPITest.php +++ b/tests/ConvertKitAPITest.php @@ -1772,15 +1772,13 @@ public function testCreateGetUpdateAndDeleteSequenceEmail() $this->assertEquals((int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], $result['email_template_id']); $this->assertEquals(false, $result['published']); $this->assertEquals(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], $result['send_days']); - $this->assertEquals(0, $result['position']); + $this->assertEquals(2, $result['position']); // Get the sequence email. $result = $this->api->get_sequence_email( sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], email_id: $sequenceEmailID ); - var_dump($result); - die(); // Update the existing sequence email. $result = $this->api->update_sequence_email( @@ -1811,7 +1809,7 @@ public function testCreateGetUpdateAndDeleteSequenceEmail() $this->assertEquals(1, $result['position']); // Delete Sequence Email. - $this->api->delete_sequence_email($sequenceID, $sequenceEmailID); + $this->api->delete_sequence_email((int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], $sequenceEmailID); $this->assertEquals(204, $this->api->getResponseInterface()->getStatusCode()); }