Webhooks allow you to receive real-time HTTP notifications when events occur in Drum. Instead of polling the API for changes, you can subscribe to specific events and Drum will push data to your endpoint automatically.
Getting Started
Creating a Webhook Subscription
Navigate to Settings > Webhooks in Drum
Click New Webhook
Enter a name for your subscription (e.g., "Accounting System Sync")
Enter your HTTPS endpoint URL
Select the events you want to receive
Click Create
Drum will generate a unique secret key for signature verification. Copy and store this securely - you'll need it to verify incoming webhooks.
Requirements
HTTPS Required: Your endpoint must use HTTPS with a valid SSL certificate
Public URL: Private/internal IP addresses (10.x.x.x, 192.168.x.x, localhost) are blocked
Response Time: Respond within 30 seconds with a 2xx status code
Available Events
Event | Description |
| A new project was created |
| An existing project was modified |
| A new invoice was created |
| An existing invoice was modified |
| A new cost was recorded |
| An existing cost was modified |
Webhook Payload
All webhooks are sent as HTTP POST requests with a JSON body.
Headers
Header | Description |
| Always |
| The event type (e.g., |
| HMAC-SHA256 signature for verification |
| Unix timestamp when the webhook was sent |
| Unique ID for this delivery attempt |
Payload Structure
{
"event": "project.created",
"timestamp": "2025-01-15T10:30:00Z",
"data": {
"id": 12345,
"type": "Project",
"attributes": {
// Event-specific attributes
}
}
}Event Payloads
Project Events
project.created and project.updated include these attributes:
{
"event": "project.created",
"timestamp": "2025-01-15T10:30:00Z",
"data": {
"id": 12345,
"type": "Project",
"attributes": {
"id": 12345,
"name": "Office Renovation",
"project_number": "PRJ-2025-001",
"project_type": "project",
"start_date": "2025-01-15",
"end_date": "2025-06-30",
"currency": "AUD",
"total_budget_cents": 15000000,
"total_invoiced_cents": 5000000,
"costs_to_date_cents": 3500000,
"team_id": 42,
"team_name": "Sydney Office",
"status": "In Progress",
"client_id": 204,
"client_name": "Acme Corporation",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
}
}Invoice Events
invoice.created and invoice.updated include these attributes:
{
"event": "invoice.created",
"timestamp": "2025-01-15T10:30:00Z",
"data": {
"id": 5678,
"type": "Invoice",
"attributes": {
"id": 5678,
"invoice_number": "INV-2025-001",
"project_id": 12345,
"project_number": "P25-001",
"status": "sent",
"currency": "AUD",
"total_cents": 550000,
"total_excluding_tax_cents": 500000,
"amount_paid_cents": 0,
"issued_date": "2025-01-15",
"due_date": "2025-02-14",
"paid_date": null,
"reference": "Progress Claim #1",
"description": "January progress claim",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
}
}Cost Events
cost.created and cost.updated include these attributes:
{
"event": "cost.created",
"timestamp": "2025-01-15T10:30:00Z",
"data": {
"id": 9012,
"type": "Cost",
"attributes": {
"id": 9012,
"name": "Steel materials",
"project_id": 12345,
"project_number": "P25-001",
"task_id": 456,
"status": "approved",
"billing_status": "billable",
"currency": "AUD",
"total_cents": 110000,
"total_excluding_tax_cents": 100000,
"issued_date": "2025-01-10",
"due_date": "2025-02-10",
"paid_date": null,
"invoice_number": "SUP-12345",
"reference": "PO-2025-042",
"description": "Structural steel for level 2",
"supplier_id": 789,
"supplier_type": "Company",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
}
}
Security
Verifying Webhook Signatures
Every webhook includes an HMAC-SHA256 signature in the X-Webhook-Signature header. You should always verify this signature to ensure the webhook came from Drum and hasn't been tampered with.
The signature is computed as:
signature = HMAC-SHA256(timestamp + "." + payload, secret)
header = "sha256=" + signature
Ruby
def verify_webhook(request, secret)
signature = request.headers["X-Webhook-Signature"]
timestamp = request.headers["X-Webhook-Timestamp"]
payload = request.raw_post
expected_signature = "sha256=" + OpenSSL::HMAC.hexdigest(
"sha256",
secret,
"#{timestamp}.#{payload}"
)
ActiveSupport::SecurityUtils.secure_compare(signature, expected_signature)
end
Python
import hmac
import hashlib
def verify_webhook(headers, body, secret):
signature = headers.get("X-Webhook-Signature")
timestamp = headers.get("X-Webhook-Timestamp")
signed_payload = f"{timestamp}.{body}"
expected = "sha256=" + hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
Node.js
const crypto = require('crypto');
function verifyWebhook(headers, body, secret) {
const signature = headers['x-webhook-signature'];
const timestamp = headers['x-webhook-timestamp'];
const signedPayload = `${timestamp}.${body}`;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Preventing Replay Attacks
Check the X-Webhook-Timestamp header and reject webhooks older than a few minutes:
def webhook_fresh?(timestamp, tolerance: 5.minutes)
webhook_time = Time.at(timestamp.to_i)
webhook_time > tolerance.ago
end
Retry Behavior
If your endpoint returns a non-2xx response or times out, Drum will retry the delivery:
Attempts: Up to 5 retries with increasing delays
Backoff: Polynomial backoff (delays increase with each retry)
Timeout: 30 seconds per request
After 10 consecutive failures across all deliveries, your webhook subscription will be automatically disabled to prevent resource waste. You'll receive an email notification when this happens.
Best Practices
Respond Quickly
Return a 2xx response as soon as you receive the webhook. Process the data asynchronously if needed:
def webhooks
# Verify signature first
return head :unauthorized unless verify_webhook(request, ENV["DRUM_WEBHOOK_SECRET"])
# Queue for async processing
WebhookProcessorJob.perform_later(params.to_json)
head :ok
end
Handle Duplicates
Webhooks may be delivered more than once. Use the X-Webhook-Delivery-Id header to deduplicate:
def process_webhook(delivery_id, payload)
return if ProcessedWebhook.exists?(delivery_id: delivery_id)
ProcessedWebhook.create!(delivery_id: delivery_id)
# Process the webhook...
end
Store Raw Payloads
Store the raw webhook payload before processing. This helps with debugging and reprocessing if needed.
Managing Subscriptions
Pausing and Resuming
You can pause a webhook subscription from the Drum UI. Paused subscriptions don't receive events but retain their configuration.
Regenerating Secrets
If your secret is compromised, regenerate it from the subscription settings. Update your endpoint with the new secret immediately - the old secret will stop working.
Viewing Delivery History
Each subscription shows recent delivery attempts with:
Status (success/failed)
Response code
Error message (if failed)
Timestamp
You can retry failed deliveries from the UI.
Troubleshooting
Webhook Not Received
Check your endpoint is publicly accessible over HTTPS
Verify the subscription is active (not paused or disabled)
Confirm you're subscribed to the correct events
Check your server logs for incoming requests
Signature Verification Failing
Ensure you're using the correct secret (check for copy/paste errors)
Verify you're using the raw request body, not parsed JSON
Confirm the timestamp and payload are concatenated correctly
Check for encoding issues (use UTF-8)
Subscription Disabled
Your subscription may be auto-disabled after 10 consecutive failures. To re-enable:
Fix the underlying issue with your endpoint
Go to Settings > Webhooks
Click on the disabled subscription
Click Resume
The failure counter resets on the next successful delivery.
Timeout Errors
If you're seeing timeout errors:
Ensure your endpoint responds within 30 seconds
Return 200 immediately and process asynchronously
Check for network issues between Drum and your server
