Webhooks API

Webhook Documentation

Receive real-time HTTP notifications when events happen in your TendedLoop environment. Connect to Slack, Zapier, your CMMS, or any custom system.

HMAC-SHA256 Signed Automatic Retries 6 Event Types

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.

Quick start: Create a webhook in the dashboard, choose your events, and point it at a URL like a RequestBin to inspect payloads. Use the "Send Test Ping" button to verify your setup.

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

  1. When you create a webhook, TendedLoop generates a unique signing secret (64-character hex string).
  2. For each delivery, TendedLoop computes HMAC-SHA256(secret, request_body) and sends it in the X-TendedLoop-Signature header.
  3. 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');
});
Important: The signing secret is only displayed once when the webhook is created. Store it securely. If you lose it, you can regenerate it from the dashboard (which invalidates the old secret).

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.