Total Access Docs
Webhooks

Outbound Webhooks

Configure outbound webhooks to receive real-time event notifications.

Outbound Webhooks

Outbound webhooks send HTTP requests to your system when events occur in Total Access. This enables real-time integrations without polling.

How it works

  1. An event occurs in Total Access (e.g. an invoice is created)
  2. Total Access matches the event against your webhook configurations
  3. If matched, an HTTP POST is sent to your endpoint URL
  4. The payload is signed with your webhook secret
  5. Your endpoint processes the event and responds with 2xx
  6. If the response is not 2xx, Total Access retries with exponential backoff

Configuration

Creating an outbound webhook

  1. Navigate to Total IntegrationOutbound Webhooks (or use the Developer Portal)
  2. Click Add Webhook
  3. Enter a name and your endpoint URL
  4. Select the events to subscribe to
  5. Choose an authentication method (HMAC recommended)
  6. Optionally configure filters (work item types, statuses)
  7. Save — a test event will be sent immediately

Authentication methods

MethodSecurityHow it works
HMAC Signature (recommended)HighSigns the raw request body with a shared secret using HMAC-SHA256
Bearer TokenMediumSends a static token in the Authorization header
Basic AuthMediumSends username:password in the Authorization header
NoneNoneNo authentication (not recommended)

HMAC signature verification

Each webhook includes an X-TotalAccess-Signature header. Verify it by computing HMAC-SHA256 of the raw body:

import crypto from 'crypto';

function verifyWebhook(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return `sha256=${expected}` === signature;
}

// Express example
app.post('/webhooks/ta', (req, res) => {
  const signature = req.headers['x-totalaccess-signature'];
  const rawBody = req.rawBody; // Use raw body, not parsed JSON

  if (!verifyWebhook(rawBody, signature, process.env.TA_WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const payload = JSON.parse(rawBody);
  // Process the event...
  res.status(200).json({ received: true });
});
import hmac
import hashlib

def verify_webhook(raw_body, signature, secret):
    expected = hmac.new(
        secret.encode('utf-8'),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    return f"sha256={expected}" == signature

# Flask example
@app.route('/webhooks/ta', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-TotalAccess-Signature')
    raw_body = request.get_data()

    if not verify_webhook(raw_body, signature, os.environ['TA_WEBHOOK_SECRET']):
        return jsonify({'error': 'Invalid signature'}), 401

    payload = request.json
    # Process the event...
    return jsonify({'received': True}), 200
function verifyWebhook($rawBody, $signature, $secret) {
    $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $signature);
}

// In your webhook handler
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_TOTALACCESS_SIGNATURE'] ?? '';

if (!verifyWebhook($rawBody, $signature, $_ENV['TA_WEBHOOK_SECRET'])) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

$payload = json_decode($rawBody, true);
// Process the event...
http_response_code(200);
echo json_encode(['received' => true]);

Always use the raw request body for signature verification. Using parsed/decoded JSON will produce a different signature.

Payload format

All outbound webhooks use a consistent envelope:

{
  "event": "invoice.created",
  "timestamp": "2026-07-22T12:00:00Z",
  "data": {
    "id": "inv_abc123",
    "item_number": "INV-2026-001",
    "status": "draft",
    "amount": 15000.00,
    "currency": "ZAR",
    "created_at": "2026-07-22T12:00:00Z"
  }
}

Headers

HeaderDescription
Content-Typeapplication/json
X-TotalAccess-Signaturesha256=<hmac> (when HMAC auth is configured)
X-TotalAccess-EventThe event type (e.g. invoice.created)
X-TotalAccess-DeliveryUnique delivery ID for idempotency
User-AgentTotalAccess-Webhook/1.0

Retry policy

Total Access retries failed deliveries with exponential backoff:

AttemptDelayCumulative
1 (initial)Immediate0s
2~1 minute~1m
3~5 minutes~6m
4~15 minutes~21m
5~1 hour~1h 21m
6 (final)~6 hours~7h 21m

A delivery is marked as failed after all 5 retries are exhausted. Your endpoint must respond with a 2xx status code within 30 seconds.

Idempotency

Each delivery includes a unique X-TotalAccess-Delivery header. Use this to deduplicate events in your system — the same event may be delivered multiple times during retries.

Filters

Filters are flat fields on the webhook configuration that restrict which events trigger a webhook delivery:

FieldExampleDescription
item_type_filter"invoice"Only fire for this work item type (e.g. invoice, bill, job)
item_subtype_filter"consulting"Only fire for this work item subtype
status_filter"paid"Only fire for this status (e.g. draft, paid, completed)

Testing

See the Webhook Testing page for using the interactive test playground.

On this page