Documentation / Webhooks

Webhooks

Get real-time notifications of events in your Tageur account.

1. Overview

Webhooks enable real-time integrations between Tageur and your external systems. When events occur in your Tageur account (like an asset being created or a task being completed), we'll automatically send HTTP POST requests to your specified endpoint URL.

Use Cases:

  • Sync asset data with your ERP system (SAP, Dynamics, Odoo, etc.)
  • Trigger automated workflows in your CMMS
  • Update inventory management systems in real-time
  • Send notifications to Slack, Teams, or custom applications
  • Maintain audit trails in external compliance systems

2. Setting Up Webhooks

To create a webhook subscription, navigate to your company settings and configure:

  1. 1. Endpoint URL: The HTTPS URL where we'll send webhook events
  2. 2. Events: Select which event types to subscribe to
  3. 3. Filters (Optional): Filter events by specific criteria (e.g., only certain asset types)
  4. 4. Timeout & Retries: Configure timeout duration (1-60 seconds) and retry attempts (0-10)

Once created, you'll receive a webhook secret used to verify the authenticity of incoming requests.

3. Available Events

Subscribe to any combination of the following event types:

Asset Events

  • asset.created Triggered when a new asset is created
  • asset.updated Triggered when asset details are modified
  • asset.deleted Triggered when an asset is deleted

Part Events

  • part.created Triggered when a new part is added to inventory
  • part.updated Triggered when part information is updated
  • part.deleted Triggered when a part is removed from inventory

Task Events

  • task.created Triggered when a new task is created
  • task.updated Triggered when task details are modified
  • task.completed Triggered when a task is marked as completed
  • task.comment.created Triggered when a comment is added to a task

Shipment Events

Growth+
  • shipment.created Triggered when a new shipment is created
  • shipment.updated Triggered when shipment details are modified
  • shipment.deleted Triggered when a shipment is deleted
  • shipment.shipped Triggered when a shipment is marked as shipped
  • shipment.delivered Triggered when a shipment is marked as delivered

4. Payload Structure

All webhook payloads follow a consistent JSON structure with event metadata and resource data.

Asset Event Payload

{
  "event": "asset.created",
  "timestamp": "2025-01-04T12:34:56Z",
  "data": {
    "id": 123,
    "type": "asset",
    "attributes": {
      "name": "Hydraulic Pump #5",
      "asset_type": "Equipment",
      "status": "operational",
      "serial_number": "HP-2025-0123",
      "brand": "Atlas Copco",
      "model_number": "GA55",
      "location": "Building A - Floor 2",
      "company_office_location_id": 45,
      "company_office_location": {
        "id": 45,
        "name": "Main Warehouse",
        "city": "Chicago",
        "state": "IL",
        "is_primary": true
      },
      "purchase_date": "2024-12-01",
      "purchase_price": "15000.00",
      "extra_values": {
        "department": "Manufacturing",
        "cost_center": "CC-100"
      },
      "created_at": "2025-01-04T12:34:56Z",
      "updated_at": "2025-01-04T12:34:56Z"
    }
  }
}

Part Event Payload

{
  "event": "part.updated",
  "timestamp": "2025-01-04T12:35:00Z",
  "data": {
    "id": 456,
    "type": "part",
    "attributes": {
      "part_number": "BRG-2025-001",
      "name": "Ball Bearing",
      "description": "High-precision ball bearing for pump motors",
      "category": "Bearings",
      "status": "in_stock",
      "vendor": "SKF",
      "manufacturer": "SKF Group",
      "unit_cost": "45.99",
      "created_at": "2024-11-15T10:20:00Z",
      "updated_at": "2025-01-04T12:35:00Z"
    }
  }
}

Task Event Payload

{
  "event": "task.completed",
  "timestamp": "2025-01-04T12:40:00Z",
  "data": {
    "id": 789,
    "type": "task",
    "attributes": {
      "title": "Monthly maintenance - Pump #5",
      "description": "Inspect and service hydraulic pump",
      "status": "completed",
      "priority": "high",
      "task_type": "maintenance",
      "due_date": "2025-01-04",
      "asset_id": 123,
      "asset_name": "Hydraulic Pump #5",
      "assigned_to_id": 456,
      "assigned_to_name": "John Smith",
      "created_by_id": 100,
      "created_by_name": "Jane Manager",
      "estimated_hours": 2.5,
      "actual_hours": 2.0,
      "started_at": "2025-01-04T10:00:00Z",
      "completed_at": "2025-01-04T12:40:00Z",
      "comments_count": 3,
      "created_at": "2025-01-01T09:00:00Z",
      "updated_at": "2025-01-04T12:40:00Z"
    }
  }
}

Task Comment Event Payload

{
  "event": "task.comment.created",
  "timestamp": "2025-01-04T11:15:00Z",
  "data": {
    "id": 456,
    "type": "comment",
    "attributes": {
      "content": "Hydraulic fluid levels checked and topped up. Pressure readings normal at 2500 PSI.",
      "task_id": 789,
      "task_title": "Monthly maintenance - Pump #5",
      "user_id": 456,
      "user_name": "John Smith",
      "created_at": "2025-01-04T11:15:00Z",
      "updated_at": "2025-01-04T11:15:00Z"
    }
  }
}

5. Security & Verification

Every webhook request includes an X-Tageur-Signature header containing an HMAC SHA-256 signature. Verify this signature to ensure requests are authentic.

Signature Verification

The signature is computed using your webhook secret and the request body:

Node.js / Express Example:

const crypto = require('crypto');

function verifyWebhookSignature(req, webhookSecret) {
  const signature = req.headers['x-tageur-signature'];
  const payload = JSON.stringify(req.body);

  const expectedSignature = crypto
    .createHmac('sha256', webhookSecret)
    .update(payload)
    .digest('hex');

  return signature === expectedSignature;
}

// Express route handler
app.post('/webhooks/tageur', (req, res) => {
  if (!verifyWebhookSignature(req, process.env.TAGEUR_WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Process webhook event
  const { event, data } = req.body;
  console.log(\`Received event: \${event}\`, data);

  res.status(200).json({ received: true });
});

Python / Flask Example:

import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

def verify_webhook_signature(request, webhook_secret):
    signature = request.headers.get('X-Tageur-Signature')
    payload = request.get_data()

    expected_signature = hmac.new(
        webhook_secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected_signature)

@app.route('/webhooks/tageur', methods=['POST'])
def handle_webhook():
    if not verify_webhook_signature(request, os.environ['TAGEUR_WEBHOOK_SECRET']):
        return jsonify({'error': 'Invalid signature'}), 401

    event_data = request.json
    event_type = event_data.get('event')
    data = event_data.get('data')

    print(f"Received event: {event_type}", data)

    return jsonify({'received': True}), 200

Security Best Practices:

  • Always verify the signature before processing webhook data
  • Use HTTPS endpoints only (required)
  • Store your webhook secret securely (environment variables, secrets manager)
  • Implement rate limiting on your webhook endpoint
  • Return 200 status code quickly, process events asynchronously

6. Retry Logic & Error Handling

Tageur implements automatic retry logic for failed webhook deliveries to ensure reliable event delivery.

Retry Behavior:

  • Timeout: Configurable from 1-60 seconds (default: 30s)
  • Max Retries: Configurable from 0-10 attempts (default: 3)
  • Retry Schedule: Exponential backoff (1m, 5m, 15m, 1h, 3h...)
  • Success Criteria: HTTP 200-299 response status

When Webhooks Fail:

Deliveries are marked as failed if:

  • Connection timeout or network error
  • HTTP status code 4xx or 5xx
  • Maximum retry attempts exceeded

Monitoring Deliveries:

View webhook delivery logs in your company settings to monitor:

  • Delivery status (success, failed, pending)
  • Response codes and error messages
  • Retry attempts and timestamps
  • Success rate metrics

7. Implementation Examples

Sync Assets to Microsoft Dynamics

// Sync new Tageur assets to Dynamics 365
app.post('/webhooks/tageur-to-dynamics', async (req, res) => {
  if (!verifyWebhookSignature(req, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { event, data } = req.body;

  if (event === 'asset.created' || event === 'asset.updated') {
    try {
      // Map Tageur asset to Dynamics entity
      const dynamicsEntity = {
        name: data.attributes.name,
        serialnumber: data.attributes.serial_number,
        assettype: data.attributes.asset_type,
        statuscode: mapStatusToDynamics(data.attributes.status),
        purchasedate: data.attributes.purchase_date,
        // Custom fields
        tageur_asset_id: data.id
      };

      // Update or create in Dynamics
      await dynamicsClient.upsert('equipment', dynamicsEntity);

      res.status(200).json({ synced: true });
    } catch (error) {
      console.error('Dynamics sync error:', error);
      res.status(500).json({ error: error.message });
    }
  } else {
    res.status(200).json({ skipped: true });
  }
});

Send Notifications to Slack

# Send task completion notifications to Slack
@app.route('/webhooks/tageur-to-slack', methods=['POST'])
def notify_slack():
    if not verify_webhook_signature(request, WEBHOOK_SECRET):
        return jsonify({'error': 'Invalid signature'}), 401

    event_data = request.json
    event_type = event_data.get('event')

    if event_type == 'task.completed':
        data = event_data.get('data', {}).get('attributes', {})

        message = {
            "text": f"✅ Task Completed: {data.get('title')}",
            "blocks": [
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": f"*Task:* {data.get('title')}\\n*Completed by:* {data.get('assigned_to_name')}\\n*Asset:* #{data.get('asset_id')}"
                    }
                }
            ]
        }

        # Send to Slack webhook
        requests.post(SLACK_WEBHOOK_URL, json=message)

    return jsonify({'received': True}), 200

Update Inventory in Custom Database

# Rails controller to sync parts inventory
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def tageur_parts
    unless verify_signature(request.body.read, request.headers['X-Tageur-Signature'])
      render json: { error: 'Invalid signature' }, status: :unauthorized
      return
    end

    event_data = JSON.parse(request.body.read)

    case event_data['event']
    when 'part.created', 'part.updated'
      sync_part_to_inventory(event_data['data'])
    when 'part.deleted'
      remove_part_from_inventory(event_data['data']['id'])
    end

    render json: { received: true }, status: :ok
  end

  private

  def sync_part_to_inventory(part_data)
    attributes = part_data['attributes']
    InventoryItem.find_or_create_by(tageur_id: part_data['id']).update(
      part_number: attributes['part_number'],
      name: attributes['name'],
      unit_cost: attributes['unit_cost'],
      vendor: attributes['vendor']
    )
  end
end

8. Testing Webhooks

Local Development

Use tools like ngrok or localtunnel to expose your local development server for webhook testing:

# Using ngrok
ngrok http 3000

# Your webhook URL becomes:
# https://abc123.ngrok.io/webhooks/tageur

Test Webhook Delivery

Use the "Test Webhook" button in your webhook settings to send a test event and verify your endpoint is configured correctly.

Testing Checklist:

  • ✓ Endpoint returns 200 OK for valid signatures
  • ✓ Endpoint rejects requests with invalid signatures (401)
  • ✓ Endpoint processes events asynchronously
  • ✓ Response time is under configured timeout
  • ✓ Idempotent handling (same event can be processed multiple times safely)

9. Bidirectional Webhooks (Receiving from External Systems)

In addition to sending webhooks to your systems, Tageur can also receive webhooks from your external ERP, CMMS, or inventory systems. This enables true bidirectional synchronization where changes in either system are automatically reflected in the other.

Use Cases:

  • Push asset updates from SAP or Dynamics when equipment is purchased or relocated
  • Sync parts inventory from your procurement system when stock levels change
  • Create maintenance tasks in Tageur when work orders are generated in your CMMS
  • Mark tasks as completed when work is finished in your external system

Endpoint and Authentication

POST https://tageur.com/api/webhooks/receive

Headers:
  Authorization: Bearer YOUR_API_KEY
  X-Tageur-Company-ID: YOUR_COMPANY_ID
  Content-Type: application/json

Why Company ID is Required:

The X-Tageur-Company-ID header contains your company's integer ID, which enables efficient database filtering. This allows Tageur to quickly authenticate your API key and process your webhook without expensive UUID lookups. You can find your company ID in your account settings under "API Integration."

Supported Events

asset.created / asset.updated

Create or update asset by serial_number (upsert operation)

asset.deleted

Mark asset as retired (soft delete)

part.created / part.updated

Create or update part by part_number (upsert operation)

part.deleted

Mark part as discontinued

task.created / task.updated

Create or update maintenance task

task.completed

Mark task as completed with timestamp

Example: Push Asset from ERP to Tageur

// Microsoft Dynamics 365 webhook integration
const axios = require('axios');

// Dynamics webhook handler
app.post('/webhooks/dynamics-to-tageur', async (req, res) => {
  const dynamicsAsset = req.body;

  // Map Dynamics fields to Tageur format
  const tageurPayload = {
    event: 'asset.created',
    timestamp: new Date().toISOString(),
    data: {
      type: 'asset',
      attributes: {
        name: dynamicsAsset.Name,
        serial_number: dynamicsAsset.SerialNumber,
        asset_type: dynamicsAsset.AssetCategory,
        status: dynamicsAsset.Status === 'Active' ? 'operational' : 'retired',
        brand: dynamicsAsset.Manufacturer,
        model_number: dynamicsAsset.ModelNumber,
        location: dynamicsAsset.Location,
        purchase_date: dynamicsAsset.PurchaseDate,
        extra_values: {
          dynamics_id: dynamicsAsset.AssetId,
          cost_center: dynamicsAsset.CostCenter,
          department: dynamicsAsset.Department,
          purchase_cost: dynamicsAsset.PurchaseCost // Store in extra_values instead
        }
      }
    }
  };

  try {
    // Send to Tageur
    const response = await axios.post(
      'https://tageur.com/api/webhooks/receive',
      tageurPayload,
      {
        headers: {
          'Authorization': `Bearer ${process.env.TAGEUR_API_KEY}`,
          'X-Tageur-Company-ID': process.env.TAGEUR_COMPANY_ID,
          'Content-Type': 'application/json'
        }
      }
    );

    console.log('Asset synced to Tageur:', response.data);
    res.status(200).json({ success: true });
  } catch (error) {
    console.error('Failed to sync to Tageur:', error.response?.data);
    res.status(500).json({ error: 'Sync failed' });
  }
});

Example: Complete Task from CMMS

# When work order is completed in your CMMS, mark task complete in Tageur
import requests
import os

def complete_tageur_task(work_order):
    """Complete Tageur task when CMMS work order is finished"""

    payload = {
        "event": "task.completed",
        "timestamp": work_order.completion_time.isoformat(),
        "data": {
            "type": "task",
            "attributes": {
                "external_id": work_order.id,  # Match by external ID
                "task_id": work_order.tageur_task_id  # Or by Tageur task ID
            }
        }
    }

    response = requests.post(
        'https://tageur.com/api/webhooks/receive',
        json=payload,
        headers={
            'Authorization': f'Bearer {os.environ["TAGEUR_API_KEY"]}',
            'X-Tageur-Company-ID': os.environ['TAGEUR_COMPANY_ID'],
            'Content-Type': 'application/json'
        }
    )

    if response.status_code == 200:
        print(f"Task completed in Tageur: {response.json()}")
    else:
        print(f"Error: {response.json()}")