Skip to main content

Drum Webhooks - Subscribe to Events in Your Drum Account

Webhooks are a way to notify third-party services about events that occur in Drum, allowing for automations that can be prompted from Drum!

Ben Walker avatar
Written by Ben Walker
Updated over a month ago

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

  1. Navigate to Settings > Webhooks in Drum

  2. Click New Webhook

  3. Enter a name for your subscription (e.g., "Accounting System Sync")

  4. Enter your HTTPS endpoint URL

  5. Select the events you want to receive

  6. 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

project.created

A new project was created

project.updated

An existing project was modified

invoice.created

A new invoice was created

invoice.updated

An existing invoice was modified

cost.created

A new cost was recorded

cost.updated

An existing cost was modified

Webhook Payload

All webhooks are sent as HTTP POST requests with a JSON body.


Headers

Header

Description

Content-Type

Always application/json

X-Webhook-Event

The event type (e.g., project.created)

X-Webhook-Signature

HMAC-SHA256 signature for verification

X-Webhook-Timestamp

Unix timestamp when the webhook was sent

X-Webhook-Delivery-Id

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

  1. Check your endpoint is publicly accessible over HTTPS

  2. Verify the subscription is active (not paused or disabled)

  3. Confirm you're subscribed to the correct events

  4. Check your server logs for incoming requests

Signature Verification Failing

  1. Ensure you're using the correct secret (check for copy/paste errors)

  2. Verify you're using the raw request body, not parsed JSON

  3. Confirm the timestamp and payload are concatenated correctly

  4. Check for encoding issues (use UTF-8)

Subscription Disabled

Your subscription may be auto-disabled after 10 consecutive failures. To re-enable:

  1. Fix the underlying issue with your endpoint

  2. Go to Settings > Webhooks

  3. Click on the disabled subscription

  4. 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

Did this answer your question?