diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dfd39d..cbaaef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## v3.4.0 - Sep 1, 2026 + +OpenAPI 1.21.9: + +- Added `workflows->delete()` to delete a workflow. +- Campaign, transactional, and workflow responses now include a `url` field pointing to the resource in the Loops app. + ## v3.3.0 - Aug 7, 2026 OpenAPI 1.21.6: diff --git a/README.md b/README.md index 26cabc4..c882cdc 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ You can use custom contact properties in API calls. Please make sure to [add cus - [workflows->create()](#workflows-create) - [workflows->get()](#workflows-get) - [workflows->update()](#workflows-update) +- [workflows->delete()](#workflows-delete) - [workflows->changeMailingList()](#workflows-changemailinglist) - [workflows->getNode()](#workflows-getnode) - [workflows->createNode()](#workflows-createnode) @@ -917,6 +918,7 @@ $result = $loops->transactional->list(per_page: 15); "data": [ { "id": "clfn0k1yg001imo0fdeqg30i8", + "url": "https://app.loops.so/transactional/clfn0k1yg001imo0fdeqg30i8", "name": "Welcome email", "draftEmailMessageId": null, "publishedEmailMessageId": "cly8k3m0n0044jpx2bghepq45", @@ -926,6 +928,7 @@ $result = $loops->transactional->list(per_page: 15); }, { "id": "cll42l54f20i1la0lfooe3z12", + "url": "https://app.loops.so/transactional/cll42l54f20i1la0lfooe3z12", "name": "Password reset", "draftEmailMessageId": "cla3r8s9t0422ua56iqovab01", "publishedEmailMessageId": "clb4s9t0u0533vb67jrpwbc12", @@ -937,6 +940,7 @@ $result = $loops->transactional->list(per_page: 15); }, { "id": "clw6rbuwp01rmeiyndm80155l", + "url": "https://app.loops.so/transactional/clw6rbuwp01rmeiyndm80155l", "name": "Team invite", "draftEmailMessageId": "clc5t0u1v0644wc78ksqxcd23", "publishedEmailMessageId": null, @@ -979,6 +983,7 @@ $result = $loops->transactional->create(name: 'Welcome email'); ```json { "id": "clfq6dinn000yl70fgwwyp82l", + "url": "https://app.loops.so/transactional/clfq6dinn000yl70fgwwyp82l", "name": "Welcome email", "draftEmailMessageId": "cly8k3m0n0044jpx2bghepq45", "draftEmailMessageContentRevisionId": "clm9n4o6p0088lrz4dijslt67", @@ -1376,6 +1381,7 @@ $result = $loops->campaigns->create( { "success": true, "campaignId": "cln4o7p9q0110msw5ekjtmv78", + "url": "https://app.loops.so/campaigns/cln4o7p9q0110msw5ekjtmv78", "name": "Spring announcement", "status": "Draft", "createdAt": "2025-01-01T00:00:00.000Z", @@ -1655,6 +1661,27 @@ $result = $loops->workflows->create( ); ``` +#### Response + +```json +{ + "id": "clw1a3b5c7d9e1f3g5h7i9j1", + "url": "https://app.loops.so/workflows/clw1a3b5c7d9e1f3g5h7i9j1", + "workflowRevisionId": null, + "status": "Draft", + "name": "Welcome series", + "description": "Onboarding emails for new signups", + "mailingListId": null, + "rootNodeId": "clt0u3v5w0232sy31kqvbzs34", + "nodes": { + "clt0u3v5w0232sy31kqvbzs34": { + "typeName": "BlankTrigger", + "nextNodeIds": ["clt0u3v5w0232sy31kqvbzs35"] + } + } +} +``` + --- ### workflows->get() @@ -1706,6 +1733,39 @@ $result = $loops->workflows->update( --- +### workflows->delete() + +Delete a workflow. + +Successful deletion returns `null` (HTTP 204 No Content). If the workflow is currently sending or has queued contacts, the API returns HTTP 409. Retry with `confirm_delete: true` to delete the workflow, stop sending, and cancel queued contacts. + +[API Reference](https://loops.so/docs/api-reference/delete-workflow) + +#### Parameters + +| Name | Type | Required | Notes | +| ------------------------- | ------- | -------- | --------------------------------------------------------------------- | +| `$workflow_id` | string | Yes | The ID of the workflow. | +| `$expected_revision_id` | string\|null | Yes | The latest workflow revision token. Pass `null` for older workflows. | +| `$confirm_delete` | boolean | No | Set to `true` after a confirmation-required 409 to confirm deleting a sending workflow or a workflow with queued contacts. | + +#### Example + +```php +$result = $loops->workflows->delete( + workflow_id: 'cls9t2u4v0210rx20jpuary23', + expected_revision_id: 'clrev1s10n2i3d4e5f6g7h8' +); + +$result = $loops->workflows->delete( + workflow_id: 'cls9t2u4v0210rx20jpuary23', + expected_revision_id: 'clrev1s10n2i3d4e5f6g7h8', + confirm_delete: true +); +``` + +--- + ### workflows->changeMailingList() Dry run or apply a workflow mailing list change. diff --git a/src/LoopsClient.php b/src/LoopsClient.php index c997ecd..5dc6ea5 100644 --- a/src/LoopsClient.php +++ b/src/LoopsClient.php @@ -128,6 +128,10 @@ public function query(string $method, string $endpoint, array $options = []): mi throw new Exceptions\APIError(statusCode: $response->getStatusCode(), json: $json); } + if ($response->getStatusCode() === 204) { + return null; + } + return json_decode(json: $response->getBody()->getContents(), associative: true); } catch (\Exception $e) { // Pass through any exceptions diff --git a/src/Workflows.php b/src/Workflows.php index 143dc41..e4b013b 100644 --- a/src/Workflows.php +++ b/src/Workflows.php @@ -61,6 +61,27 @@ public function update( ]); } + public function delete( + string $workflow_id, + ?string $expected_revision_id, + ?bool $confirm_delete = null + ): mixed { + $payload = array_merge( + ['expectedRevisionId' => $expected_revision_id], + Util::omitNull([ + 'confirmDelete' => $confirm_delete, + ]) + ); + + return $this->client->query( + method: 'DELETE', + endpoint: 'v1/workflows/' . $workflow_id, + options: [ + 'json' => $payload + ] + ); + } + public function changeMailingList( string $workflow_id, ?string $expected_revision_id, diff --git a/tests/CampaignsTest.php b/tests/CampaignsTest.php index f29f49d..9706fd4 100644 --- a/tests/CampaignsTest.php +++ b/tests/CampaignsTest.php @@ -54,6 +54,7 @@ public function testCreateCampaign(): void body: json_encode([ 'success' => true, 'campaignId' => 'camp_123', + 'url' => 'https://app.loops.so/campaigns/camp_123', 'name' => 'Spring announcement', 'status' => 'Draft', 'createdAt' => '2025-01-01T00:00:00.000Z', diff --git a/tests/TransactionalTest.php b/tests/TransactionalTest.php index 3bd488d..affd8bd 100644 --- a/tests/TransactionalTest.php +++ b/tests/TransactionalTest.php @@ -137,6 +137,7 @@ public function testListTransactionals(): void 'data' => [ [ 'id' => 'clfn0k1yg001imo0fdeqg30i8', + 'url' => 'https://app.loops.so/transactional/clfn0k1yg001imo0fdeqg30i8', 'name' => 'Welcome email', 'draftEmailMessageId' => null, 'publishedEmailMessageId' => 'msg_abc123', @@ -171,6 +172,7 @@ public function testListTransactionals(): void $this->assertIsArray($result['data']); $this->assertNotEmpty($result['data']); $this->assertArrayHasKey('id', $result['data'][0]); + $this->assertArrayHasKey('url', $result['data'][0]); $this->assertArrayHasKey('name', $result['data'][0]); $this->assertArrayHasKey('draftEmailMessageId', $result['data'][0]); $this->assertArrayHasKey('publishedEmailMessageId', $result['data'][0]); @@ -194,6 +196,7 @@ public function testCreateTransactional(): void status: 201, body: json_encode([ 'id' => 'txn_123', + 'url' => 'https://app.loops.so/transactional/txn_123', 'name' => 'Welcome email', 'draftEmailMessageId' => 'msg_123', 'draftEmailMessageContentRevisionId' => 'rev_123', @@ -244,6 +247,7 @@ public function testGetTransactional(): void status: 200, body: json_encode([ 'id' => $transactionalId, + 'url' => 'https://app.loops.so/transactional/' . $transactionalId, 'name' => 'Welcome email', 'draftEmailMessageId' => 'msg_123', 'publishedEmailMessageId' => 'msg_456', diff --git a/tests/WorkflowsTest.php b/tests/WorkflowsTest.php index c0d7c8c..42c011a 100644 --- a/tests/WorkflowsTest.php +++ b/tests/WorkflowsTest.php @@ -56,6 +56,7 @@ public function testCreateWorkflow(): void status: 200, body: json_encode([ 'id' => 'wf_123', + 'url' => 'https://app.loops.so/workflows/wf_123', 'name' => 'Welcome series', 'status' => 'Draft', ]) @@ -104,6 +105,58 @@ public function testUpdateWorkflow(): void $this->assertEquals('Updated name', $result['name']); } + public function testDeleteWorkflow(): void + { + $workflowId = 'wf_123'; + + $this->mockHttpClient + ->expects($this->once()) + ->method('delete') + ->with( + 'v1/workflows/' . $workflowId, + $this->callback(function ($options) { + return $options['json'] === [ + 'expectedRevisionId' => 'rev_123', + ]; + }) + ) + ->willReturn(new Response(status: 204)); + + $result = $this->client->workflows->delete( + workflow_id: $workflowId, + expected_revision_id: 'rev_123' + ); + + $this->assertNull($result); + } + + public function testDeleteWorkflowWithConfirmDelete(): void + { + $workflowId = 'wf_123'; + + $this->mockHttpClient + ->expects($this->once()) + ->method('delete') + ->with( + 'v1/workflows/' . $workflowId, + $this->callback(function ($options) { + return $options['json'] === [ + 'expectedRevisionId' => 'rev_123', + 'confirmDelete' => true, + ]; + }) + ) + ->willReturn(new Response(status: 204)); + + $result = $this->client->workflows->delete( + workflow_id: $workflowId, + expected_revision_id: 'rev_123', + confirm_delete: true + ); + + $this->assertNull($result); + } + public function testChangeMailingList(): void { $workflowId = 'wf_123';