Plate ← Docs

Webhooks

Receive real-time HTTP notifications when tasks are created, updated, or completed — no polling required.

Overview

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:

Webhooks are available on all plans at no extra charge. There is no limit on the number of endpoints per workspace.

Managing webhooks

Webhook endpoints are managed via the REST API. Authenticate with your API key.

POST /api/v1/webhooks

Registers a new webhook endpoint.

FieldTypeDescription
url*stringHTTPS endpoint that will receive event payloads
secret*stringShared secret used to sign payloads. Store it securely — Plate never returns it again.
eventsoptionalstring[]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"] }
GET /api/v1/webhooks

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" }]
DELETE /api/v1/webhooks/:id

Deletes a webhook endpoint. Returns 204 on success.

Payload

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:

HeaderDescription
X-Plate-EventEvent type: task.created, task.updated, or task.completed
X-Plate-DeliveryUnique event ID (same as id in the payload). Use this to deduplicate retries.
X-Plate-SignatureHMAC-SHA256 signature of the raw JSON body (see below)
User-AgentPlate-Webhook/1.0

Signature verification

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.

Node.js

// 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();
});

Python

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
Always verify the signature before processing an event. Always use a constant-time comparison to prevent timing attacks.

Retries

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:

AttemptDelay after previous failure
2nd5 minutes
3rd30 minutes
4th2 hours
5th8 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.

Deduplication

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