Webhook API Documentation

Connect TradesOps to your favorite tools and workflows. Receive real-time notifications when events happen in your account -- invoices paid, quotes accepted, projects completed, and more.

35+ EventsHMAC-SHA256 SignaturesAutomatic RetriesZapier & Make Ready

Getting Started

TradesOps webhooks let you subscribe to events that happen in your organization. When an event occurs (e.g., an invoice is paid), we send an HTTP POST request to your configured URL with the event details.

Quick start in 3 steps:

  1. 1Register a webhook endpoint -- Go to Settings > Webhooks in your dashboard, or use the POST /api/webhooks endpoint.
  2. 2Choose your events -- Select which events you want to receive. You can subscribe to all events or pick specific ones.
  3. 3Save your secret -- When you create a webhook, you will receive a signing secret. Store it securely -- you will need it to verify that incoming requests are genuinely from TradesOps.

Important

Your webhook secret is only shown once when the webhook is created. Make sure to copy and store it securely. If you lose it, you will need to delete the webhook and create a new one.

Authentication

Webhook management endpoints require session authentication (you must be logged in). All webhook API calls are scoped to your organization -- you can only manage webhooks that belong to your org.

API Endpoints

GET/api/webhooks-- List all webhooks
POST/api/webhooks-- Create a webhook
PATCH/api/webhooks/:id-- Update a webhook
DELETE/api/webhooks/:id-- Delete a webhook
POST/api/webhooks/:id/test-- Send a test ping
POST/api/webhooks/test-- Test any URL with a ping

Create Webhook Request

json
{
  "name": "My Integration",
  "url": "https://example.com/webhooks/platform",
  "events": [
    "invoice.paid",
    "quote.accepted",
    "project.completed"
  ],
  "isActive": true
}

The response will include the webhook object with a secret field. This is the only time the secret is returned.

Payload Format

Every webhook delivery uses the same envelope format. The payload is sent as a JSON POST request with Content-Type: application/json.

json
{
  "event": "invoice.paid",
  "data": {
    "id": "clx1abc2300001",
    "invoiceNumber": "INV-0042",
    "clientName": "Acme Construction LLC",
    "total": 12500,
    "amountPaid": 12500,
    "currency": "USD",
    "status": "PAID",
    "paidAt": "2026-03-20T09:15:00.000Z",
    "paymentMethod": "stripe"
  },
  "timestamp": "2026-03-20T09:15:01.234Z",
  "webhookId": "clxwh001",
  "attempt": 1
}
FieldTypeDescription
eventstringThe event type (e.g., invoice.paid)
dataobjectEvent-specific payload data (see event reference below)
timestampstringISO 8601 timestamp of when the event was fired
webhookIdstringThe ID of the webhook subscription that triggered this delivery
attemptnumberDelivery attempt number (1 = first attempt, 2+ = retries)

HTTP Headers

Every webhook delivery includes these headers:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature of the raw request body: sha256=<hex>
X-Webhook-EventThe event type (same as the event field in the body)
X-Webhook-IdThe webhook subscription ID
X-Webhook-AttemptCurrent delivery attempt number
Content-TypeAlways application/json

Webhook Events

TradesOps supports 57 webhook events across 23 categories. Each event includes a data field with the relevant entity details.

Quick Reference

EventCategory
invoice.createdInvoices
invoice.sentInvoices
invoice.paidInvoices
invoice.overdueInvoices
quote.createdQuotes
quote.sentQuotes
quote.acceptedQuotes
quote.rejectedQuotes
project.createdProjects
project.status_changedProjects
project.completedProjects
contact.createdContacts
contact.stage_changedContacts
payment.receivedPayments
contract.signedContracts
expense.createdExpenses
schedule.createdScheduling
equipment.createdEquipment
equipment.updatedEquipment
equipment.deletedEquipment
vendor.createdVendors
vendor.updatedVendors
vendor.deletedVendors
incident.createdIncidents
incident.updatedIncidents
incident.deletedIncidents
permit.createdPermits
permit.updatedPermits
permit.deletedPermits
estimate.createdEstimates
estimate.updatedEstimates
timesheet.status_changedTimesheets
daily_log.createdDaily Logs
daily_log.updatedDaily Logs
purchase_order.sentPurchase Orders
tax_rate.createdSettings
tax_rate.updatedSettings
quote.signedQuotes
booking.createdBookings
booking.confirmedBookings
booking.cancelledBookings
portal.quote_approvedPortal
portal.quote_rejectedPortal
portal.change_order_approvedPortal
portal.message_sentPortal
milestone.invoicedPayments
inventory.low_stockInventory
import.completedSettings
rfq.createdRFQs
rfq.response_receivedRFQs
rfq.awardedRFQs
warranty_claim.submittedWarranties
warranty_claim.resolvedWarranties
project.budget_alertProjects
contact.credit_checkedContacts
meeting.createdMeetings
meeting.transcribedMeetings

Invoices

invoice.created

Fired when a new invoice is created in the system.

Invoices

Example data payload

json
{
  "id": "clx1abc2300001",
  "invoiceNumber": "INV-0042",
  "clientName": "Acme Construction LLC",
  "contactId": "clx1abc2300002",
  "projectId": "clx1abc2300003",
  "total": 12500,
  "currency": "USD",
  "status": "DRAFT",
  "dueDate": "2026-04-15T00:00:00.000Z",
  "lineItems": [
    {
      "description": "Foundation pour - 200 sq ft",
      "quantity": 200,
      "unitPrice": 45,
      "total": 9000
    },
    {
      "description": "Rebar installation",
      "quantity": 1,
      "unitPrice": 3500,
      "total": 3500
    }
  ]
}
invoice.sent

Fired when an invoice is sent to the client via email.

Invoices

Example data payload

json
{
  "id": "clx1abc2300001",
  "invoiceNumber": "INV-0042",
  "clientName": "Acme Construction LLC",
  "contactId": "clx1abc2300002",
  "total": 12500,
  "currency": "USD",
  "status": "SENT",
  "sentAt": "2026-03-18T14:30:00.000Z",
  "recipientEmail": "billing@acme-construction.com"
}
invoice.paid

Fired when an invoice is fully paid (payment received and matched).

Invoices

Example data payload

json
{
  "id": "clx1abc2300001",
  "invoiceNumber": "INV-0042",
  "clientName": "Acme Construction LLC",
  "total": 12500,
  "amountPaid": 12500,
  "currency": "USD",
  "status": "PAID",
  "paidAt": "2026-03-20T09:15:00.000Z",
  "paymentMethod": "stripe"
}
invoice.overdue

Fired when an invoice passes its due date without full payment.

Invoices

Example data payload

json
{
  "id": "clx1abc2300001",
  "invoiceNumber": "INV-0042",
  "clientName": "Acme Construction LLC",
  "total": 12500,
  "amountPaid": 0,
  "currency": "USD",
  "status": "OVERDUE",
  "dueDate": "2026-03-15T00:00:00.000Z",
  "daysOverdue": 3
}

Quotes

quote.created

Fired when a new quote/estimate is created.

Quotes

Example data payload

json
{
  "id": "clx2def4500001",
  "quoteNumber": "QTE-0018",
  "clientName": "Pacific Builders Inc",
  "contactId": "clx2def4500002",
  "total": 28750,
  "currency": "USD",
  "status": "DRAFT",
  "validUntil": "2026-04-17T00:00:00.000Z"
}
quote.sent

Fired when a quote is sent to the client for review.

Quotes

Example data payload

json
{
  "id": "clx2def4500001",
  "quoteNumber": "QTE-0018",
  "clientName": "Pacific Builders Inc",
  "total": 28750,
  "currency": "USD",
  "status": "SENT",
  "sentAt": "2026-03-18T10:00:00.000Z"
}
quote.accepted

Fired when a client accepts a quote.

Quotes

Example data payload

json
{
  "id": "clx2def4500001",
  "quoteNumber": "QTE-0018",
  "clientName": "Pacific Builders Inc",
  "total": 28750,
  "currency": "USD",
  "status": "ACCEPTED",
  "acceptedAt": "2026-03-19T16:45:00.000Z"
}
quote.rejected

Fired when a client rejects a quote.

Quotes

Example data payload

json
{
  "id": "clx2def4500001",
  "quoteNumber": "QTE-0018",
  "clientName": "Pacific Builders Inc",
  "total": 28750,
  "currency": "USD",
  "status": "REJECTED",
  "rejectedAt": "2026-03-19T16:45:00.000Z",
  "reason": "Budget constraints"
}
quote.signed

Fired when a client signs a quote via the e-sign link.

Quotes

Example data payload

json
{
  "id": "clx002",
  "quoteNumber": "QTE-0020",
  "signerName": "John Doe",
  "signedAt": "2026-03-18T14:00:00.000Z"
}

Projects

project.created

Fired when a new project is created.

Projects

Example data payload

json
{
  "id": "clx3ghi6700001",
  "name": "Downtown Office Foundation",
  "clientName": "Acme Construction LLC",
  "contactId": "clx1abc2300002",
  "status": "PLANNING",
  "startDate": "2026-04-01T00:00:00.000Z",
  "estimatedEndDate": "2026-06-30T00:00:00.000Z",
  "budget": 150000,
  "currency": "USD"
}
project.status_changed

Fired when a project transitions to a new status (e.g., Planning to In Progress).

Projects

Example data payload

json
{
  "id": "clx3ghi6700001",
  "name": "Downtown Office Foundation",
  "previousStatus": "PLANNING",
  "newStatus": "IN_PROGRESS",
  "changedAt": "2026-04-01T08:00:00.000Z",
  "changedBy": "John Smith"
}
project.completed

Fired when a project is marked as completed.

Projects

Example data payload

json
{
  "id": "clx3ghi6700001",
  "name": "Downtown Office Foundation",
  "status": "COMPLETED",
  "completedAt": "2026-06-28T17:00:00.000Z",
  "totalCost": 142500,
  "budget": 150000,
  "currency": "USD"
}
project.budget_alert

Fired when project costs exceed alert thresholds.

Projects

Example data payload

json
{
  "projectId": "clx015",
  "alertType": "budget_exceeded",
  "threshold": 80,
  "currentValue": 85
}

Contacts

contact.created

Fired when a new contact (lead or client) is added.

Contacts

Example data payload

json
{
  "id": "clx4jkl8900001",
  "name": "Jane Doe",
  "email": "jane@example.com",
  "phone": "+1-555-0123",
  "company": "Pacific Builders Inc",
  "stage": "LEAD",
  "source": "website"
}
contact.stage_changed

Fired when a contact moves through the CRM pipeline (e.g., Lead to Qualified).

Contacts

Example data payload

json
{
  "id": "clx4jkl8900001",
  "name": "Jane Doe",
  "previousStage": "LEAD",
  "newStage": "QUALIFIED",
  "changedAt": "2026-03-18T11:30:00.000Z"
}
contact.credit_checked

Fired when a credit check is performed on a contact.

Contacts

Example data payload

json
{
  "contactId": "clx016",
  "score": 720,
  "riskLevel": "low"
}

Payments

payment.received

Fired when a payment is recorded against an invoice.

Payments

Example data payload

json
{
  "id": "clx5mno0100001",
  "invoiceId": "clx1abc2300001",
  "invoiceNumber": "INV-0042",
  "amount": 12500,
  "currency": "USD",
  "method": "stripe",
  "receivedAt": "2026-03-20T09:15:00.000Z",
  "clientName": "Acme Construction LLC"
}
milestone.invoiced

Fired when a payment milestone generates an invoice.

Payments

Example data payload

json
{
  "milestoneId": "clx007",
  "invoiceId": "clx008",
  "amount": 25000
}

Contracts

contract.signed

Fired when a contract is signed by the client (via the public signing page).

Contracts

Example data payload

json
{
  "id": "clx6pqr2300001",
  "contractNumber": "CTR-0007",
  "clientName": "Acme Construction LLC",
  "contactId": "clx1abc2300002",
  "projectId": "clx3ghi6700001",
  "total": 150000,
  "currency": "USD",
  "status": "SIGNED",
  "signedAt": "2026-03-18T14:00:00.000Z",
  "signerName": "Bob Johnson",
  "signerEmail": "bob@acme-construction.com"
}

Expenses

expense.created

Fired when a new expense is recorded.

Expenses

Example data payload

json
{
  "id": "clx7stu4500001",
  "description": "Concrete delivery - 20 yards",
  "amount": 2800,
  "currency": "USD",
  "category": "Materials",
  "projectId": "clx3ghi6700001",
  "projectName": "Downtown Office Foundation",
  "vendor": "Ready Mix Co",
  "date": "2026-03-18T00:00:00.000Z"
}

Scheduling

schedule.created

Fired when a new schedule entry is created for a project.

Scheduling

Example data payload

json
{
  "id": "clx8vwx6700001",
  "title": "Foundation Pour - Phase 1",
  "projectId": "clx3ghi6700001",
  "projectName": "Downtown Office Foundation",
  "startDate": "2026-04-05T07:00:00.000Z",
  "endDate": "2026-04-05T16:00:00.000Z",
  "assignedTo": [
    "John Smith",
    "Mike Davis"
  ]
}

Equipment

equipment.created

Fired when a new equipment item is added to the inventory.

Equipment

Example data payload

json
{
  "id": "clx9yza8900001",
  "name": "CAT 320 Excavator",
  "type": "Excavator",
  "status": "AVAILABLE",
  "serialNumber": "CAT320-2024-001",
  "purchaseDate": "2024-06-15T00:00:00.000Z",
  "purchasePrice": 185000
}
equipment.updated

Fired when equipment details or status are updated.

Equipment

Example data payload

json
{
  "id": "clx9yza8900001",
  "name": "CAT 320 Excavator",
  "status": "IN_USE",
  "changes": {
    "status": {
      "from": "AVAILABLE",
      "to": "IN_USE"
    }
  }
}
equipment.deleted

Fired when an equipment item is removed from the system.

Equipment

Example data payload

json
{
  "id": "clx9yza8900001",
  "name": "CAT 320 Excavator",
  "deletedAt": "2026-03-18T12:00:00.000Z"
}

Vendors

vendor.created

Fired when a new vendor/supplier is added.

Vendors

Example data payload

json
{
  "id": "clxabcd0100001",
  "name": "Ready Mix Co",
  "email": "orders@readymix.com",
  "phone": "+1-555-0456",
  "category": "Concrete Supplier"
}
vendor.updated

Fired when vendor details are updated.

Vendors

Example data payload

json
{
  "id": "clxabcd0100001",
  "name": "Ready Mix Co",
  "changes": {
    "phone": {
      "from": "+1-555-0456",
      "to": "+1-555-0789"
    }
  }
}
vendor.deleted

Fired when a vendor is removed from the system.

Vendors

Example data payload

json
{
  "id": "clxabcd0100001",
  "name": "Ready Mix Co",
  "deletedAt": "2026-03-18T12:00:00.000Z"
}

Incidents

incident.created

Fired when a safety incident or report is created.

Incidents

Example data payload

json
{
  "id": "clxbefg2300001",
  "title": "Minor equipment malfunction",
  "severity": "LOW",
  "projectId": "clx3ghi6700001",
  "projectName": "Downtown Office Foundation",
  "reportedBy": "Mike Davis",
  "reportedAt": "2026-03-18T10:30:00.000Z"
}
incident.updated

Fired when an incident report is updated (e.g., resolution added).

Incidents

Example data payload

json
{
  "id": "clxbefg2300001",
  "title": "Minor equipment malfunction",
  "severity": "LOW",
  "status": "RESOLVED",
  "resolvedAt": "2026-03-18T15:00:00.000Z"
}
incident.deleted

Fired when an incident report is deleted.

Incidents

Example data payload

json
{
  "id": "clxbefg2300001",
  "title": "Minor equipment malfunction",
  "deletedAt": "2026-03-18T12:00:00.000Z"
}

Permits

permit.created

Fired when a new permit is added to a project.

Permits

Example data payload

json
{
  "id": "clxchij4500001",
  "permitNumber": "BP-2026-0042",
  "type": "Building Permit",
  "projectId": "clx3ghi6700001",
  "projectName": "Downtown Office Foundation",
  "status": "PENDING",
  "issuedBy": "City of Portland",
  "expiresAt": "2027-03-18T00:00:00.000Z"
}
permit.updated

Fired when a permit status or details are updated.

Permits

Example data payload

json
{
  "id": "clxchij4500001",
  "permitNumber": "BP-2026-0042",
  "status": "APPROVED",
  "approvedAt": "2026-03-20T09:00:00.000Z"
}
permit.deleted

Fired when a permit is removed from the system.

Permits

Example data payload

json
{
  "id": "clxchij4500001",
  "permitNumber": "BP-2026-0042",
  "deletedAt": "2026-03-18T12:00:00.000Z"
}

Estimates

estimate.created

Fired when a new cost estimate is created.

Estimates

Example data payload

json
{
  "id": "clxdklm6700001",
  "name": "Foundation Work Estimate",
  "projectId": "clx3ghi6700001",
  "total": 45000,
  "currency": "USD",
  "status": "DRAFT"
}
estimate.updated

Fired when an estimate is revised or updated.

Estimates

Example data payload

json
{
  "id": "clxdklm6700001",
  "name": "Foundation Work Estimate",
  "total": 47500,
  "currency": "USD",
  "status": "REVISED",
  "previousTotal": 45000
}

Timesheets

timesheet.status_changed

Fired when a timesheet is submitted, approved, or rejected.

Timesheets

Example data payload

json
{
  "id": "clxenop8900001",
  "employeeName": "John Smith",
  "employeeId": "clxenop8900002",
  "weekStarting": "2026-03-16T00:00:00.000Z",
  "totalHours": 42.5,
  "previousStatus": "SUBMITTED",
  "newStatus": "APPROVED",
  "approvedBy": "Jane Manager"
}

Daily Logs

daily_log.created

Fired when a daily site log is created for a project.

Daily Logs

Example data payload

json
{
  "id": "clxfqrs0100001",
  "projectId": "clx3ghi6700001",
  "projectName": "Downtown Office Foundation",
  "date": "2026-03-18T00:00:00.000Z",
  "weather": "Clear, 72F",
  "crewSize": 8,
  "summary": "Completed foundation forms for section B. Rebar inspection passed."
}
daily_log.updated

Fired when a daily log entry is updated.

Daily Logs

Example data payload

json
{
  "id": "clxfqrs0100001",
  "projectId": "clx3ghi6700001",
  "date": "2026-03-18T00:00:00.000Z",
  "updatedFields": [
    "summary",
    "crewSize"
  ]
}

Purchase Orders

purchase_order.sent

Fired when a purchase order is sent to a vendor.

Purchase Orders

Example data payload

json
{
  "id": "clxgtuv2300001",
  "poNumber": "PO-0015",
  "vendorName": "Ready Mix Co",
  "vendorId": "clxabcd0100001",
  "projectId": "clx3ghi6700001",
  "total": 5600,
  "currency": "USD",
  "status": "SENT",
  "sentAt": "2026-03-18T09:00:00.000Z"
}

Settings

tax_rate.created

Fired when a new tax rate is created.

Settings

Example data payload

json
{
  "id": "clx001",
  "name": "State Sales Tax",
  "rate": 0.0825
}
tax_rate.updated

Fired when a tax rate is modified.

Settings

Example data payload

json
{
  "id": "clx001",
  "name": "State Sales Tax",
  "rate": 0.09
}
import.completed

Fired when a data import finishes processing.

Settings

Example data payload

json
{
  "id": "clx010",
  "source": "csv",
  "entityType": "contacts",
  "totalRows": 500,
  "processedRows": 498,
  "failedRows": 2
}

Bookings

booking.created

Fired when a new booking is created through the public page.

Bookings

Example data payload

json
{
  "id": "clx003",
  "name": "Jane Smith",
  "email": "jane@example.com",
  "date": "2026-04-01",
  "startTime": "09:00"
}
booking.confirmed

Fired when a booking is confirmed.

Bookings

Example data payload

json
{
  "id": "clx003",
  "status": "confirmed"
}
booking.cancelled

Fired when a booking is cancelled.

Bookings

Example data payload

json
{
  "id": "clx003",
  "status": "cancelled"
}

Portal

portal.quote_approved

Fired when a client approves a quote through the portal.

Portal

Example data payload

json
{
  "quoteId": "clx004",
  "approvedBy": "Client User"
}
portal.quote_rejected

Fired when a client rejects a quote through the portal.

Portal

Example data payload

json
{
  "quoteId": "clx004",
  "rejectedBy": "Client User",
  "reason": "Too expensive"
}
portal.change_order_approved

Fired when a client approves a change order through the portal.

Portal

Example data payload

json
{
  "changeOrderId": "clx005",
  "approvedBy": "Client User"
}
portal.message_sent

Fired when a client sends a message through the portal.

Portal

Example data payload

json
{
  "messageId": "clx006",
  "content": "Question about timeline"
}

Inventory

inventory.low_stock

Fired when an inventory item falls below reorder point.

Inventory

Example data payload

json
{
  "id": "clx009",
  "name": "Rebar #4",
  "currentStock": 50,
  "reorderPoint": 100
}

RFQs

rfq.created

Fired when a new RFQ is created.

RFQs

Example data payload

json
{
  "id": "clx011",
  "rfqNumber": "RFQ-0001",
  "title": "Concrete Materials"
}
rfq.response_received

Fired when a vendor submits an RFQ response.

RFQs

Example data payload

json
{
  "rfqId": "clx011",
  "vendorId": "clx012",
  "totalAmount": 15000
}
rfq.awarded

Fired when an RFQ is awarded to a vendor.

RFQs

Example data payload

json
{
  "rfqId": "clx011",
  "vendorId": "clx012",
  "poId": "clx013"
}

Warranties

warranty_claim.submitted

Fired when a client submits a warranty claim.

Warranties

Example data payload

json
{
  "claimId": "clx014",
  "title": "Crack in foundation"
}
warranty_claim.resolved

Fired when a warranty claim is resolved.

Warranties

Example data payload

json
{
  "claimId": "clx014",
  "resolution": "Repaired"
}

Meetings

meeting.created

Fired when a new meeting note is created.

Meetings

Example data payload

json
{
  "id": "clx017",
  "title": "Client kickoff",
  "projectId": "clx015"
}
meeting.transcribed

Fired when a meeting recording is transcribed.

Meetings

Example data payload

json
{
  "id": "clx017",
  "duration": 3600
}

Signature Verification

Every webhook delivery is signed with HMAC-SHA256 using your webhook's secret. The signature is included in the X-Webhook-Signature header as sha256=<hex_digest>.

You should always verify the signature to ensure the request genuinely came fromTradesOps and has not been tampered with.

How signature verification works:

  1. Read the raw request body as a string (do not parse it first)
  2. Compute the HMAC-SHA256 digest of the raw body using your webhook secret
  3. Compare the computed digest with the value in the X-Webhook-Signature header (after removing the sha256= prefix)
  4. Use a timing-safe comparison function to prevent timing attacks

Node.js / Express

javascript
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  const received = signature.replace('sha256=', '');

  // Use timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(received, 'hex')
  );
}

// Express.js example
app.post('/webhooks/platform', (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const rawBody = JSON.stringify(req.body);

  if (!verifyWebhookSignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

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

  // Process the event...

  res.status(200).send('OK');
});

Python / Flask

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

app = Flask(__name__)
WEBHOOK_SECRET = 'your_webhook_secret_here'

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()

    received = signature.replace('sha256=', '')
    return hmac.compare_digest(expected, received)

@app.route('/webhooks/platform', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Webhook-Signature', '')
    raw_body = request.get_data()

    if not verify_signature(raw_body, signature, WEBHOOK_SECRET):
        return jsonify({'error': 'Invalid signature'}), 401

    data = request.get_json()
    event = data.get('event')
    print(f'Received event: {event}')

    # Process the event...

    return jsonify({'status': 'ok'}), 200

PHP

php
<?php
$secret = getenv('WEBHOOK_SECRET');
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

$data = json_decode($payload, true);
$event = $data['event'];

// Process the event...
error_log("Received webhook event: $event");

http_response_code(200);
echo json_encode(['status' => 'ok']);

Security Warning

Always verify signatures in production. Never skip verification, even for testing. Use a timing-safe comparison (e.g., crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python, or hash_equals in PHP) to prevent timing attacks.

Retry Policy

If your endpoint returns a non-2xx status code or the request times out,TradesOps will automatically retry the delivery with exponential backoff.

AttemptDelayNotes
1 (initial)ImmediateFirst delivery attempt
2 (retry 1)5 seconds5,000 ms * 5^0
3 (retry 2)25 seconds5,000 ms * 5^1
4 (retry 3)~2 minutes5,000 ms * 5^2 = 125 seconds

Request Timeout

Each delivery attempt has a 10-second timeout. If your endpoint does not respond within 10 seconds, it is treated as a failure.

Auto-Disable

After 10 consecutive failures (across any events), the webhook is automatically disabled. You can re-enable it from the settings page.

What counts as a successful delivery?

Any HTTP response with a 2xx status code (200-299) is considered successful. The response body is logged but not validated -- you can return whatever you want. If you return a non-2xx status or the connection fails, the delivery will be retried.

Testing Webhooks

TradesOps provides several ways to test your webhook integration before going live.

Test from Dashboard

Each webhook in Settings > Webhooks has a test button that sends a test.ping event to your endpoint. The delivery result (status code, response time, response body) is shown immediately.

Test API Endpoint

Use the POST /api/webhooks/test endpoint to send a test ping to any URL without creating a persistent webhook:

json
{
  "url": "https://your-endpoint.com/webhook",
  "secret": "optional_secret_for_signing"
}

If you omit the secret field, a temporary secret will be generated and returned in the response.

Test Ping Payload

The test.ping event looks like this:

json
{
  "event": "test.ping",
  "data": {
    "message": "This is a test webhook from TradesOps. If you receive this, your endpoint is working correctly.",
    "testId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "timestamp": "2026-03-18T14:30:00.000Z"
  },
  "timestamp": "2026-03-18T14:30:00.000Z",
  "webhookId": "test_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "attempt": 1
}

Recommended Testing Tools

webhook.site

Free tool that gives you a unique URL to receive and inspect webhook payloads. Great for debugging.

webhook.site

ngrok

Exposes your local development server to the internet. Perfect for testing webhooks against your local code.

ngrok.com

Zapier Integration

Connect TradesOps to 6,000+ apps using Zapier. Use TradesOps webhooks as a trigger to automate workflows -- send Slack messages when invoices are paid, create Google Sheets rows for new quotes, update your CRM when projects are completed, and more.

Step-by-step setup:

  1. 1

    Create a Zap

    In Zapier, click "Create Zap" and search for "Webhooks by Zapier" as the trigger app. Choose "Catch Hook" as the trigger event.

  2. 2

    Copy the Zapier webhook URL

    Zapier will give you a unique URL like https://hooks.zapier.com/hooks/catch/123456/abcdef/. Copy this URL.

  3. 3

    Register in TradesOps

    Go to Settings > Webhooks in your TradesOps dashboard. Click "Add Webhook", paste the Zapier URL, give it a name (e.g., "Zapier - Invoice Notifications"), and select the events you want to receive.

  4. 4

    Send a test event

    Click the test button (paper plane icon) next to your webhook in TradesOps. Then go back to Zapier and click "Test trigger" -- it should find the test event.

  5. 5

    Add an action

    Choose what happens when the webhook fires. For example: send a Slack message, create a Google Sheets row, send an email, update HubSpot, etc. Map the webhook data fields to your action fields.

  6. 6

    Turn on your Zap

    Test the full workflow, then publish your Zap. It will now run automatically whenever the subscribed events fire in TradesOps.

Zapier Tips

Filter by event type

If you subscribe to multiple events, add a Zapier Filter step after the trigger. Filter on event equals invoice.paid (or whichever event you want that specific Zap to handle).

Access nested data

Webhook data is nested under data. In Zapier's field mapping, look for fields like data__invoiceNumber or data__total.

Create separate Zaps for each event

For cleaner automation, create a separate webhook endpoint in TradesOps for each workflow. E.g., one webhook for "Slack notifications on invoice.paid" and another for "Google Sheets log on quote.accepted".

Make (Integromat) Integration

Make (formerly Integromat) is a powerful visual automation platform. Use TradesOpswebhooks to trigger Make scenarios and connect with hundreds of apps.

Step-by-step setup:

  1. 1

    Create a new scenario

    In Make, create a new scenario. Click the "+" button to add a module and search for "Webhooks". Select "Custom webhook".

  2. 2

    Create and copy the webhook URL

    Click "Add" to create a new webhook in Make. Give it a name (e.g., "TradesOps Events") and copy the generated URL. It will look like https://hook.make.com/abc123xyz....

  3. 3

    Register in TradesOps

    Go to Settings > Webhooks in your dashboard. Create a new webhook with the Make URL and select your desired events.

  4. 4

    Determine the data structure

    Back in Make, click "Re-determine data structure" (or it may wait automatically). Then send a test event from TradesOps. Make will parse the incoming payload and learn its structure.

  5. 5

    Add a Router (optional)

    If you subscribe to multiple events, add a Router module after the webhook. Create separate routes with filters like event equals invoice.paid. Each route can trigger different actions.

  6. 6

    Add action modules

    Add modules for your desired actions: send emails, update spreadsheets, create records in other apps, etc. Map the webhook data fields to each module's inputs.

  7. 7

    Activate the scenario

    Test the scenario with the sample data, then toggle it to "ON". Make will now process incoming webhooks in real time.

Make Tips

Verify signatures in Make

Make supports webhook signature verification. In the webhook module settings, enable "Webhook verification" and configure HMAC-SHA256 with your TradesOpswebhook secret. Set the header name to X-Webhook-Signature and the prefix to sha256=.

Handle retries gracefully

Make processes each webhook delivery independently. If TradesOps retries a failed delivery, Make will process it as a new execution. Check the attempt field in the payload if you need to detect retries.

Use data stores for deduplication

If idempotency is important, use a Make Data Store to track processed event IDs. Check the webhookId + event + timestamp combination to detect duplicates.

Best Practices

Respond quickly

Return a 200 response as soon as possible. Process the webhook asynchronously (e.g., add to a queue) rather than blocking the response. TradesOps has a 10-second timeout per delivery.

Handle duplicates

Due to retries, you may receive the same event more than once. Design your handler to be idempotent -- processing the same event twice should produce the same result.

Verify signatures

Always verify the X-Webhook-Signature header in production to ensure requests come from TradesOps.

Use HTTPS endpoints

Always use HTTPS for your webhook endpoint in production. This ensures the payload (including your data) is encrypted in transit.

Monitor delivery logs

Check the delivery logs in Settings > Webhooks regularly. If you see failures, investigate and fix them before the webhook is auto-disabled.

Subscribe selectively

Only subscribe to the events you need. This reduces unnecessary traffic and processing on your end.