Webhook Documentation
Receive real-time HTTP notifications when events happen in your TendedLoop environment. Connect to Slack, Zapier, your CMMS, or any custom system.
Overview
Webhooks let you subscribe to events in TendedLoop and receive instant HTTP POST notifications at a URL you specify. When an event occurs (e.g., a new feedback submission or a task being completed), TendedLoop sends a JSON payload to your endpoint.
Webhooks are available on Pro plans and above. You can configure them from the Webhooks settings page in your dashboard.
Authentication & Verification
Every webhook delivery includes an X-TendedLoop-Signature header containing an HMAC-SHA256 signature. You should verify this signature to confirm the request originated from TendedLoop and has not been tampered with.
How it works
- When you create a webhook, TendedLoop generates a unique signing secret (64-character hex string).
- For each delivery, TendedLoop computes
HMAC-SHA256(secret, request_body)and sends it in theX-TendedLoop-Signatureheader. - Your server recomputes the HMAC using the same secret and the raw request body, then compares.
Verification example (Node.js)
import { createHmac } from 'crypto'; function verifySignature(rawBody, signature, secret) { const expected = createHmac('sha256', secret) .update(rawBody) .digest('hex'); return signature === expected; } // In your Express handler: app.post('/webhooks/tendedloop', (req, res) => { const signature = req.headers['x-tendedloop-signature']; if (!verifySignature(req.rawBody, signature, WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } // Process the event... res.status(200).send('OK'); });
Payload Format
All webhook payloads follow the same envelope structure:
{
"event": "feedback.received",
"timestamp": "2026-02-13T10:30:45.123Z",
"data": {
// Event-specific payload (see Event Catalog below)
}
}
| Field | Type | Description |
|---|---|---|
event |
string | The event type (e.g., task.created) |
timestamp |
string | ISO 8601 datetime when the event occurred |
data |
object | Event-specific payload (varies by event type) |
Event Catalog
TendedLoop currently fires 6 event types, plus a ping test event. Select which events to subscribe to when creating a webhook.
feedback.received
Fired when new feedback is submitted via a QR code scan.
{
"event": "feedback.received",
"timestamp": "2026-02-13T10:30:45.123Z",
"data": {
"feedback": {
"id": "clx1abc...",
"rating": 3,
"comment": "Soap dispenser empty"
},
"amenity": {
"id": "clx2def...",
"name": "2F Restroom"
},
"building": {
"id": "clx3ghi..."
}
}
}
status.reported
Fired when an amenity status report is submitted (e.g., paper towel dispenser empty).
{
"event": "status.reported",
"timestamp": "2026-02-13T11:15:00.000Z",
"data": {
"statusReport": {
"id": "clx4jkl...",
"newStatus": "EMPTY",
"previousStatus": "OK"
},
"amenity": {
"id": "clx2def..."
},
"building": {
"id": "clx3ghi..."
}
}
}
task.created
Fired when a new task is created from feedback or a status report.
{
"event": "task.created",
"timestamp": "2026-02-13T10:30:46.000Z",
"data": {
"task": {
"id": "clx5mno...",
"status": "NEW",
"priority": "CRITICAL",
"assignedUserId": "clx6pqr..."
},
"source": "feedback",
"amenity": {
"id": "clx2def...",
"typeId": "clx7stu..."
},
"building": {
"id": "clx3ghi..."
}
}
}
task.acknowledged
Fired when a task status changes to IN_PROGRESS (technician starts working on it).
{
"event": "task.acknowledged",
"timestamp": "2026-02-13T10:45:00.000Z",
"data": {
"task": {
"id": "clx5mno...",
"status": "IN_PROGRESS",
"priority": "CRITICAL",
"assignedUserId": "clx6pqr..."
}
}
}
task.completed
Fired when a task is marked as completed.
{
"event": "task.completed",
"timestamp": "2026-02-13T11:30:00.000Z",
"data": {
"task": {
"id": "clx5mno...",
"status": "COMPLETED",
"priority": "CRITICAL",
"assignedUserId": "clx6pqr..."
}
}
}
task.reassigned
Fired when one or more tasks are reassigned to a different team member.
{
"event": "task.reassigned",
"timestamp": "2026-02-13T12:00:00.000Z",
"data": {
"taskIds": ["clx5mno...", "clx8vwx..."],
"assignedToId": "clx9yza..."
}
}
ping
Test only
Sent when you click "Send Test Ping" in the dashboard. Use it to verify your endpoint is receiving deliveries correctly.
{
"event": "ping",
"timestamp": "2026-02-13T10:00:00.000Z",
"data": {
"test": true,
"message": "This is a test webhook delivery from TendedLoop"
}
}
Retry Behavior
If your endpoint returns a non-2xx status code or the request times out (10-second limit), TendedLoop automatically retries the delivery using exponential backoff.
| Attempt | Delay | Cumulative Wait |
|---|---|---|
| 1 (initial) | Immediate | 0s |
| 2 | 30 seconds | 30s |
| 3 | 2 minutes | 2m 30s |
| 4 | 15 minutes | 17m 30s |
| 5 | 1 hour | 1h 17m 30s |
| 6 (final) | 4 hours | 5h 17m 30s |
After 5 failed retries (6 total attempts), the delivery is marked as failed. You can view delivery history and status codes in the dashboard.
Headers Reference
Every webhook delivery includes the following HTTP headers:
| Header | Example | Description |
|---|---|---|
Content-Type |
application/json |
Always JSON |
X-TendedLoop-Event |
feedback.received |
The event type that triggered this delivery |
X-TendedLoop-Signature |
a1b2c3d4... |
HMAC-SHA256 hex digest of the request body |
Best Practices
Respond quickly with 200
Return a 2xx response within 10 seconds. Process the payload asynchronously (e.g., push to a queue) to avoid timeouts.
Always verify signatures
Check the X-TendedLoop-Signature header on every request to prevent spoofed deliveries.
Handle duplicates gracefully
In rare cases, the same event may be delivered more than once. Use the data IDs to deduplicate.
Use HTTPS endpoints
Always use HTTPS for your webhook URLs to encrypt data in transit. HTTP URLs will be automatically upgraded.
Monitor delivery health
Check the delivery history in the dashboard periodically. Persistent failures may indicate a misconfigured endpoint.
Subscribe only to events you need
Reduce noise and processing load by selecting only the event types relevant to your integration.
Code Examples
JS Node.js (Express)
import express from 'express'; import { createHmac } from 'crypto'; const app = express(); const WEBHOOK_SECRET = process.env.TENDEDLOOP_WEBHOOK_SECRET; // IMPORTANT: Use raw body for signature verification app.post('/webhooks/tendedloop', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-tendedloop-signature']; const rawBody = req.body.toString(); // Verify signature const expected = createHmac('sha256', WEBHOOK_SECRET) .update(rawBody) .digest('hex'); if (signature !== expected) { console.error('Invalid webhook signature'); return res.status(401).send('Unauthorized'); } // Parse and handle the event const event = JSON.parse(rawBody); console.log(`Received: ${event.event}`); switch (event.event) { case 'feedback.received': // Post to Slack, create JIRA ticket, etc. console.log(`New feedback: ${event.data.feedback.rating}/5`); break; case 'task.created': console.log(`New task: ${event.data.task.id}`); break; default: console.log(`Unhandled event: ${event.event}`); } res.status(200).json({ received: true }); } ); app.listen(3000);
PY Python (Flask)
import hmac, hashlib, json from flask import Flask, request, jsonify app = Flask(__name__) WEBHOOK_SECRET = os.environ['TENDEDLOOP_WEBHOOK_SECRET'] @app.route('/webhooks/tendedloop', methods=['POST']) def handle_webhook(): # Verify signature signature = request.headers.get('X-TendedLoop-Signature', '') raw_body = request.get_data(as_text=True) expected = hmac.new( WEBHOOK_SECRET.encode(), raw_body.encode(), hashlib.sha256 ).hexdigest() if not hmac.compare_digest(signature, expected): return jsonify({'error': 'Invalid signature'}), 401 # Parse and handle the event event = json.loads(raw_body) event_type = event['event'] if event_type == 'feedback.received': rating = event['data']['feedback']['rating'] print(f'New feedback: {rating}/5') elif event_type == 'task.created': task_id = event['data']['task']['id'] print(f'New task: {task_id}') return jsonify({'received': True}), 200
Ready to integrate?
Create your first webhook in under a minute from the dashboard.