|
1 | 1 | ---
|
2 | 2 | title: Webhooks
|
3 | 3 | sidebar_label: Overview
|
4 |
| -description: A webhook triggers a HTTP POST request on the specified url, whenever there is a change in an event. Like a new project is created, updated or deleted then a webhook can be triggered to receive the required payload. |
| 4 | +description: Use webhooks to receive real-time notifications about events in Plane. |
5 | 5 | ---
|
6 | 6 |
|
7 | 7 | {frontMatter.description && <h3 class="description">{frontMatter.description}</h3>}
|
8 | 8 |
|
9 |
| -## Creating a webhook |
| 9 | +## Introduction |
10 | 10 |
|
11 |
| -`url` You are required to provide a url in which you want the payloads to be triggered. |
| 11 | +Webhooks are a powerful way to receive real-time HTTP-based notifications about |
| 12 | +events in Plane. By setting up a webhook, you can automatically receive updates |
| 13 | +whenever specific events occur, such as the creation, update, or deletion of |
| 14 | +projects, work items, or other entities. |
12 | 15 |
|
13 |
| -Then select the events for which you want the webhook to be triggered. |
| 16 | +:::tip Events |
| 17 | +Plane supports the following webhook events: |
14 | 18 |
|
15 |
| -After you create the webhook, a secret key will be created automatically and will be downloaded in csv format. |
| 19 | +- **Project**: created, updated, deleted |
| 20 | +- **Cycle**: created, updated, deleted |
| 21 | +- **Module**: created, updated, deleted |
| 22 | +- **Work Item**: created, updated, deleted, added to cycle, added to module |
| 23 | +- **Work Item Comment**: created, updated, deleted |
| 24 | +::: |
16 | 25 |
|
17 |
| -```json |
18 |
| -"Content-Type": "application/json", |
19 |
| -"User-Agent": "Autopilot", |
20 |
| -"X-Plane-Delivery": "f819eff4-cd50-4987-bc97-e5be1e04c94f", |
21 |
| -"X-Plane-Event": "project", |
22 |
| -"X-Plane-Signature": "7896ae9addb1f73931132b4f3e052bf12c410b837b24898e75dcd660c7" |
23 | 26 |
|
24 |
| -``` |
| 27 | +## Creating a webhook |
25 | 28 |
|
26 |
| -| Header | Description | |
27 |
| -| ----------------- | -------------------------------------------------------------------- | |
28 |
| -| X-Plane-Delivery | It is a randomly generated UUID for uniquely identifying the payload | |
29 |
| -| X-Plane-Event | It describes the event for which the webhook triggered | |
30 |
| -| X-Plane-Signature | A signature is generated based on the secret and the payload | |
| 29 | +To create a webhook, navigate to your Plane workspace settings, **Developer** |
| 30 | +section, and click **Webhooks**. Then click the **Add webhook** button. |
31 | 31 |
|
32 |
| -### Webhook Payload Example for the project `update` |
| 32 | +import ImgCreate from '@site/static/images/webhooks/create.png'; |
33 | 33 |
|
34 |
| -```json |
35 |
| -"event": "project", |
36 |
| -"action": "update", |
37 |
| -"webhook_id": "3c2c32ac-82df-48b3-be2a-a3e21dbe8692", |
38 |
| -"workspace_id": "d2d97c94-a6ad-4012-b526-5577c0d7c769", |
39 |
| -"data": { |
40 |
| - "id":"22b6fc9c-1849-45da-b103-52a3e3a6b4c1", |
41 |
| - "workspace_detail": { |
42 |
| - "name":"Testing Project", |
43 |
| - "slug":"testing-project", |
44 |
| - "id":"bob1b192-f988-4bf9-b569-825de8cb0678" |
45 |
| - }, |
46 |
| - "created_at":"2023-10-25T04:38:59.566962Z", |
47 |
| - "updated_at":"2023-10-25T06:44:48.543685Z", |
48 |
| - "name":"vfecddcwerj", |
49 |
| - "description":"", |
50 |
| - "description_text":null, |
51 |
| - "description_html":null, |
52 |
| - "network":2, |
53 |
| - "identifier":"TRACE", |
54 |
| - "emoji":null, |
55 |
| - "icon_prop":null, |
56 |
| - "module_view":true, |
57 |
| - "cycle_view":true, |
58 |
| - "issue_views_view":true, |
59 |
| - "page_view":true, |
60 |
| - "inbox_view":true, |
61 |
| - "cover_image":null, |
62 |
| - "archive_in":0, |
63 |
| - "close_in":0, |
64 |
| - "created_by":"6bb20d1c-4960-41ca-af4f-cee01de160c4", |
65 |
| - "updated_by":"6bb20d1c-4960-41ca-af4f-cee01de160c4", |
66 |
| - "workspace":"bob1b192-f988-4bf9-b569-825de8cb0678", |
67 |
| - "default_assignee":null, |
68 |
| - "project_lead":null, |
69 |
| - "estimate":null, |
70 |
| - "default_state":null |
71 |
| -}, |
| 34 | +<img src={ImgCreate} className='center' style={{width: '700px'}} alt="Create webhook" /> |
72 | 35 |
|
73 |
| -``` |
| 36 | +Next, enter your URL, optionally choose the events which will trigger |
| 37 | +your webhook and click **Create**. |
74 | 38 |
|
75 |
| -User can choose the things for which they want the webhook to be triggered. |
| 39 | +import ImgDetails from '@site/static/images/webhooks/details.png'; |
76 | 40 |
|
77 |
| -Currently Plane supports the following events for which webhook can be trigged: |
| 41 | +<img src={ImgDetails} className='center' style={{width: '700px'}} alt="Define webhook details" /> |
78 | 42 |
|
79 |
| - - Project |
80 |
| - - Issue |
81 |
| - - Cycle |
82 |
| - - Module |
83 |
| - - Issue Comment |
| 43 | +Afterwards, you will be prompted to save your webhook secret key. You can use |
| 44 | +this key to verify the authenticity of webhook payloads you receive from Plane. |
| 45 | +We'll cover this [later in the guide](#verifying-payloads). |
84 | 46 |
|
85 |
| -## Verifying Signature |
| 47 | +import ImgKey from '@site/static/images/webhooks/key.png'; |
86 | 48 |
|
87 |
| -```python |
88 |
| -import hashlib |
89 |
| -import hmac |
| 49 | +<img src={ImgKey} className='center' style={{width: '700px'}} alt="Webhook key created" /> |
90 | 50 |
|
91 |
| -secret_token = os.environ.get("WEBHOOK_SECRET") |
| 51 | +## Defining a webhook consumer |
92 | 52 |
|
93 |
| -received_signature = request.headers.get('X-Plane-Signature') |
94 |
| -received_payload = json.dumps(request.json).encode('utf-8') |
| 53 | +Your webhook consumer URL must be an HTTP endpoint which satisfies the following conditions: |
95 | 54 |
|
96 |
| -expected_signature = hmac.new(secret_token.encode('utf-8'), msg=received_payload, digestmod=hashlib.sha256).hexdigest() |
| 55 | +1. Responds to Plane requests with an `HTTP 200` ("OK") response |
| 56 | +2. Is publicly accessible |
| 57 | +3. Is not localhost |
| 58 | + - Valid: `https://example.com/webhook` |
| 59 | + - Invalid: `http://localhost:3000/webhook` |
97 | 60 |
|
98 |
| -if not hmac.compare_digest(expected_signature, received_signature): |
99 |
| - raise HTTPException(status_code=403, detail="Invalid Signature provided") |
| 61 | +:::tip |
| 62 | +If your consumer does not respond with an `HTTP 200` response, Plane will retry the delivery |
| 63 | +up to three times with an exponential backoff delay. |
| 64 | +::: |
100 | 65 |
|
101 |
| -``` |
| 66 | +## Webhook payloads |
102 | 67 |
|
103 |
| -## How webhooks work |
| 68 | +When a webhook is triggered, Plane sends a JSON payload to your specified URL via HTTP `POST`. |
| 69 | +The payload contains the following fields: |
104 | 70 |
|
105 |
| -Your webhook consumer is a simple HTTP endpoint. It must satisfy the following conditions: |
| 71 | +- `action`: One of `create`, `update`, or `delete`, indicating the type of event |
| 72 | +- `event`: One of `project`, `cycle`, `module`, or `issue`, indicating the type of event that triggered the webhook |
| 73 | + - _Note: `issue` refers to Work Items in Plane. This will be updated in the future to reflect the correct terminology._ |
| 74 | +- `webhook_id`: The unique identifier of the webhook |
| 75 | +- `workspace_id`: The ID of the workspace where the event occurred |
| 76 | +- `data`: An optional object containing the details of the event. The structure |
| 77 | +of this field varies based on the `event` and `action` (see below). |
106 | 78 |
|
107 |
| -- It's available in a publicly accessible non-localhost URL. |
108 |
| -- It will respond to the Plane Webhook push (HTTP POST request) with a `HTTP 200` ("OK") response. |
| 79 | +### Payload examples |
109 | 80 |
|
110 |
| -If a delivery fails (i.e. server unavailable or responded with a non-200 HTTP status code), the push will be retried a couple of times. Here an exponential backoff delay is used: the attempt will be retried after approximately 10 minutes, then 30 minutes, and so on. |
| 81 | +#### Delete action |
111 | 82 |
|
112 |
| -The webhooks are triggered for POST, PATCH, and DELETE requests. |
| 83 | +```json |
| 84 | +{ |
| 85 | + "event":"issue", |
| 86 | + "action":"delete", |
| 87 | + "webhook_id":"f1a2fe64-c8d4-4eed-b3ef-498690052c1d", |
| 88 | + "workspace_id":"c467e125-59e3-44ec-b5ee-f9c1e138c611", |
| 89 | + "data":{ |
| 90 | + "id":"9a28bd00-ed9c-4f5d-8be9-fc05cbb1fc57" |
| 91 | + } |
| 92 | +} |
| 93 | +``` |
113 | 94 |
|
114 |
| -- For DELETE requests, the response only includes the ID of the deleted entity. |
| 95 | +#### Update action |
115 | 96 |
|
| 97 | +```json |
| 98 | +{ |
| 99 | + "event":"project", |
| 100 | + "action":"update", |
| 101 | + "webhook_id":"3c2c32ac-82df-48b3-be2a-a3e21dbe8692", |
| 102 | + "workspace_id":"d2d97c94-a6ad-4012-b526-5577c0d7c769", |
| 103 | + "data": { |
| 104 | + "id":"22b6fc9c-1849-45da-b103-52a3e3a6b4c1", |
| 105 | + "workspace_detail": { |
| 106 | + "name":"Testing Project", |
| 107 | + "slug":"testing-project", |
| 108 | + "id":"bob1b192-f988-4bf9-b569-825de8cb0678" |
| 109 | + }, |
| 110 | + "created_at":"2023-10-25T04:38:59.566962Z", |
| 111 | + "updated_at":"2023-10-25T06:44:48.543685Z", |
| 112 | + "name":"vfecddcwerj", |
| 113 | + "description":"", |
| 114 | + "description_text":null, |
| 115 | + "description_html":null, |
| 116 | + "network":2, |
| 117 | + "identifier":"TRACE", |
| 118 | + "emoji":null, |
| 119 | + "icon_prop":null, |
| 120 | + "module_view":true, |
| 121 | + "cycle_view":true, |
| 122 | + "issue_views_view":true, |
| 123 | + "page_view":true, |
| 124 | + "inbox_view":true, |
| 125 | + "cover_image":null, |
| 126 | + "archive_in":0, |
| 127 | + "close_in":0, |
| 128 | + "created_by":"6bb20d1c-4960-41ca-af4f-cee01de160c4", |
| 129 | + "updated_by":"6bb20d1c-4960-41ca-af4f-cee01de160c4", |
| 130 | + "workspace":"bob1b192-f988-4bf9-b569-825de8cb0678", |
| 131 | + "default_assignee":null, |
| 132 | + "project_lead":null, |
| 133 | + "estimate":null, |
| 134 | + "default_state":null |
| 135 | + } |
| 136 | +} |
116 | 137 | ```
|
117 |
| -"action":"delete", |
118 |
| -"data":{ |
119 |
| - "id":"9a28bd00-ed9c-4f5d-8be9-fc05cbb1fc57" |
120 |
| -}, |
121 |
| -"event":"issue", |
122 |
| -"webhook_id":"f1a2fe64-c8d4-4eed-b3ef-498690052c1d", |
123 |
| -"workspace_id":"c467e125-59e3-44ec-b5ee-f9c1e138c611" |
124 | 138 |
|
125 |
| -``` |
| 139 | +## Webhook headers |
126 | 140 |
|
127 |
| -- However, for both POST and PATCH requests, the complete payload is sent in the response. |
| 141 | +When Plane sends a webhook payload, it includes several HTTP headers to help you |
| 142 | +identify and verify the request. The most important headers are: |
128 | 143 |
|
| 144 | +```json |
| 145 | +"Content-Type": "application/json", |
| 146 | +"X-Plane-Delivery": "f819eff4-cd50-4987-bc97-e5be1e04c94f", |
| 147 | +"X-Plane-Event": "project", |
| 148 | +"X-Plane-Signature": "7896ae9addb1f73931132b4f3e052bf12c410b837b24898e75dcd660c7" |
129 | 149 | ```
|
130 |
| -"event":"issue", |
131 |
| -"action":"update", |
132 |
| -"webhook_id":"f1a2fe64-c8d4-4eed-b3ef-498690052c1d", |
133 |
| -"workspace_id":"c467e125-59e3-44ec-b5ee-f9c1e138c611", |
134 |
| -"data":{ ... } |
135 | 150 |
|
136 |
| -``` |
| 151 | +| Header | Description | |
| 152 | +| ----------------- | -------------------------------------------------------------------- | |
| 153 | +| X-Plane-Delivery | A randomly generated UUID for uniquely identifying the payload | |
| 154 | +| X-Plane-Event | The event for which the webhook was triggered | |
| 155 | +| X-Plane-Signature | A signature generated based on the secret and the payload | |
| 156 | + |
| 157 | + |
| 158 | +## Verifying payloads |
| 159 | + |
| 160 | +To verify the authenticity of the webhook payloads, you can use the `X-Plane-Signature` header. |
| 161 | +This header contains an HMAC SHA-256 signature of the payload, generated using the secret key |
| 162 | +you received when creating the webhook. If the signature matches the expected signature, you can be confident that the payload |
| 163 | +was sent by Plane and has not been tampered with. |
| 164 | + |
| 165 | +Here's how to verify the payload signature in Python: |
| 166 | + |
| 167 | +import Tabs from '@theme/Tabs'; |
| 168 | +import TabItem from '@theme/TabItem'; |
| 169 | + |
| 170 | +<Tabs> |
| 171 | + <TabItem value="NodeJS" label="NodeJS" default> |
| 172 | + ```javascript |
| 173 | + // Fastify example |
| 174 | + import { createHmac } from 'node:crypto'; |
| 175 | + const secret = process.env.WEBHOOK_SECRET; |
| 176 | + |
| 177 | + // Fastify setup excluded for brevity |
| 178 | + |
| 179 | + fastify.post('/webhook', options, async (request, reply) => { |
| 180 | + const receivedSignature = request.headers['x-plane-signature']; |
| 181 | + const payload = JSON.stringify(request.body); |
| 182 | + |
| 183 | + const computedSignature = createHmac('sha256', secret) |
| 184 | + .update(payload) |
| 185 | + .digest('hex'); |
| 186 | + |
| 187 | + if (receivedSignature !== computedSignature) { |
| 188 | + return reply.status(403).send('Invalid signature'); |
| 189 | + } |
| 190 | + |
| 191 | + // Process the webhook payload |
| 192 | + return reply.status(200).send('Webhook received'); |
| 193 | + }) |
| 194 | + ``` |
| 195 | + </TabItem> |
| 196 | + <TabItem value="python" label="Python"> |
| 197 | + ```python |
| 198 | + import hashlib |
| 199 | + import hmac |
| 200 | + |
| 201 | + secret_token = os.environ.get("WEBHOOK_SECRET") |
| 202 | + |
| 203 | + received_signature = request.headers.get('X-Plane-Signature') |
| 204 | + received_payload = json.dumps(request.json).encode('utf-8') |
| 205 | + |
| 206 | + expected_signature = hmac.new(secret_token.encode('utf-8'), msg=received_payload, digestmod=hashlib.sha256).hexdigest() |
| 207 | + |
| 208 | + if not hmac.compare_digest(expected_signature, received_signature): |
| 209 | + raise HTTPException(status_code=403, detail="Invalid Signature provided") |
| 210 | + ``` |
| 211 | + </TabItem> |
| 212 | +</Tabs> |
137 | 213 |
|
138 |
| -:::note |
139 |
| -Whenever an issue is added to the module, the corresponding issue webhook will be triggered. Similarly, any updates made to the cycle issue will also activate the issue webhook. |
| 214 | +:::tip Questions? |
| 215 | +If you have any questions or need help with webhooks, please reach out to us at |
| 216 | + |
140 | 217 | :::
|
0 commit comments