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
- An event occurs in Total Access (e.g. an invoice is created)
- Total Access matches the event against your webhook configurations
- If matched, an HTTP POST is sent to your endpoint URL
- The payload is signed with your webhook secret
- Your endpoint processes the event and responds with
2xx - If the response is not
2xx, Total Access retries with exponential backoff
Configuration
Creating an outbound webhook
- Navigate to Total Integration → Outbound Webhooks (or use the Developer Portal)
- Click Add Webhook
- Enter a name and your endpoint URL
- Select the events to subscribe to
- Choose an authentication method (HMAC recommended)
- Optionally configure filters (work item types, statuses)
- Save — a test event will be sent immediately
Authentication methods
| Method | Security | How it works |
|---|---|---|
| HMAC Signature (recommended) | High | Signs the raw request body with a shared secret using HMAC-SHA256 |
| Bearer Token | Medium | Sends a static token in the Authorization header |
| Basic Auth | Medium | Sends username:password in the Authorization header |
| None | None | No 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}), 200function 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
| Header | Description |
|---|---|
Content-Type | application/json |
X-TotalAccess-Signature | sha256=<hmac> (when HMAC auth is configured) |
X-TotalAccess-Event | The event type (e.g. invoice.created) |
X-TotalAccess-Delivery | Unique delivery ID for idempotency |
User-Agent | TotalAccess-Webhook/1.0 |
Retry policy
Total Access retries failed deliveries with exponential backoff:
| Attempt | Delay | Cumulative |
|---|---|---|
| 1 (initial) | Immediate | 0s |
| 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:
| Field | Example | Description |
|---|---|---|
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.