Skip to main content

Drum webhooks

Subscribe to Drum events, verify signatures, handle retries and correlate opportunity conversion events.

Written by Ben Walker

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

opportunity.won

An opportunity moved to a won status (its deal_won_date was set)

opportunity.lost

An opportunity moved to a lost status (its deal_lost_date was set)

Opportunities are stored as projects with project_type: "opportunity", so project.created and project.updated fire for opportunities as well as for delivery projects. Use the project_type attribute in the payload to tell them apart.

opportunity.won and opportunity.lost fire once per outcome: the first time the relevant date moves from empty to set. Moving a won opportunity back to a standard status clears the date and fires only project.updated; a later move back to won fires opportunity.won again.

Converting an opportunity into a project emits three events: project.created for the new project, plus project.updated and opportunity.won for the opportunity. Delivery order is not guaranteed. Correlate them with the opportunity_id attribute on the new project, which holds the id of the opportunity it was converted from.

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, project.updated, opportunity.won, and opportunity.lost all use the project payload and 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,
"client_id": 789,
"status": "In Progress",
"client_name": "Acme Corporation",
"team_name": "Engineering",
"opportunity_id": 12001,
"opportunity_number": "OPP-2025-014",
"deal_won_date": null,
"deal_lost_date": null,
"lead_score": null,
"sales_status_type": "standard",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
}
}

Attribute

Description

opportunity_id

For a project converted from an opportunity, the id of that opportunity; otherwise null

opportunity_number

The project number of the source opportunity, or null

deal_won_date

When the opportunity was marked won, or null

deal_lost_date

When the opportunity was marked lost, or null

lead_score

The name of the opportunity's lead score, or null

sales_status_type

The type of the current status: standard, won, or lost

The payload never includes the opportunity's estimated amount, final deal amount, or estimated deposit.

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": "PRJ-2025-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": "PRJ-2025-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?