Receive real-time HTTP notifications when tasks are created, updated, or completed — no polling required.
Register an HTTPS endpoint and Plate will POST a signed JSON payload to it whenever a matching task event occurs in your workspace. Each change produces exactly one event — the types are mutually exclusive:
task.created — a new task was createdtask.completed — a task was marked as done (takes priority over task.updated)task.updated — any other change to a task: name, assignee, status, section, description, labels, or re-opening a completed taskWebhooks are available on all plans at no extra charge. There is no limit on the number of endpoints per workspace.
Webhook endpoints are managed via the REST API. Authenticate with your API key.
Registers a new webhook endpoint.
| Field | Type | Description |
|---|---|---|
url* | string | HTTPS endpoint that will receive event payloads |
secret* | string | Shared secret used to sign payloads. Store it securely — Plate never returns it again. |
eventsoptional | string[] | Which events to deliver. Omit or send [] to subscribe to all three events. |
curl https://plate.to/api/v1/webhooks \ -H "X-API-Key: plt_your_key" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com/hooks/plate","secret":"your_secret"}' # Returns 201 { "id": "wh_abc", "url": "https://example.com/hooks/plate", "events": ["task.created", "task.updated", "task.completed"] }
Returns all registered endpoints for the workspace. Secrets are never included.
[{ "id": "wh_abc", "url": "https://example.com/hooks/plate", "events": ["task.created", "task.updated", "task.completed"], "createdAt": "2024-01-15T10:00:00.000Z" }]
Deletes a webhook endpoint. Returns 204 on success.
Plate sends an HTTP POST with Content-Type: application/json and the following body:
{
"id": "evt_550e8400-e29b-41d4-a716-446655440000",
"event": "task.completed",
"createdAt": "2024-01-15T10:30:00.000Z",
"data": {
"id": "task_xyz",
"number": 42,
"name": "Deploy to production",
"isCompleted": true,
"description": null,
"listId": "list_abc",
"projectId": "proj_abc",
"assigneeId": "user_abc",
"statusId": "status_done",
"labels": [],
"createdAt": "2024-01-14T08:00:00.000Z"
}
}
description is null when the task has no description. When set, it is a structured rich text array — concatenate the text fields from all leaf nodes to extract plain text.
Every request includes these headers:
| Header | Description |
|---|---|
X-Plate-Event | Event type: task.created, task.updated, or task.completed |
X-Plate-Delivery | Unique event ID (same as id in the payload). Use this to deduplicate retries. |
X-Plate-Signature | HMAC-SHA256 signature of the raw JSON body (see below) |
User-Agent | Plate-Webhook/1.0 |
Every payload is signed with HMAC-SHA256 using your webhook secret. Verify the signature before processing any event.
The X-Plate-Signature header has the format sha256=<hex>. Compute the expected signature by running HMAC-SHA256 over the raw request body bytes using your secret, then compare in constant time.
// Express example const crypto = require('crypto'); app.post('/hooks/plate', express.raw({type: 'application/json'}), (req, res) => { const sig = req.headers['x-plate-signature']; const expected = 'sha256=' + crypto .createHmac('sha256', process.env.PLATE_WEBHOOK_SECRET) .update(req.body) .digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(req.body); // handle event... res.status(200).send(); });
import hmac, hashlib, os from flask import Flask, request app = Flask(__name__) @app.route('/hooks/plate', methods=['POST']) def webhook(): sig = request.headers.get('X-Plate-Signature', '') expected = 'sha256=' + hmac.new( os.environ['PLATE_WEBHOOK_SECRET'].encode(), request.get_data(), hashlib.sha256 ).hexdigest() if not hmac.compare_digest(sig, expected): return 'Invalid signature', 401 event = request.get_json() # handle event... return '', 200
Plate considers a delivery successful when your endpoint returns a 2xx status within 10 seconds. If delivery fails — due to a non-2xx response, a timeout, or a connection error — Plate retries automatically with exponential backoff:
| Attempt | Delay after previous failure |
|---|---|
| 2nd | 5 minutes |
| 3rd | 30 minutes |
| 4th | 2 hours |
| 5th | 8 hours |
After 5 failed attempts the delivery is marked as permanently failed and no further retries are made. If your endpoint was down for an extended period, use GET /api/v1/projects/:id/tasks to manually sync the current state of your workspace.
Each event has a unique ID in X-Plate-Delivery and in the payload id field. If your endpoint returns a non-2xx response and the request is retried, all retry attempts carry the same event ID. Store seen IDs to make your handler idempotent.
Questions? Write to us at hello@plate.to