From dd1679563e8d5dfcfa015f4f62b00828b77ef7ae Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 15:12:26 +0800 Subject: [PATCH 1/6] 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 38535dfdc4e472feb7f2fcaff9052ffef8a789b9 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 15:42:06 +0800 Subject: [PATCH 2/6] 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 f16feb46ddb59756db3e810babfaaea59b2362e0 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 16:34:39 +0800 Subject: [PATCH 3/6] 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 7063af2f0c94985e65e0f0b22fc60b36032f1e9b Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 18 May 2026 20:07:33 +0800 Subject: [PATCH 4/6] 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 5/6] 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 6/6] 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()); }