Your app is a system of record: deals close, bookings confirm, leads arrive. Your customers want to react to those events in real time—trigger a Zapier workflow, sync to a custom CRM, fire a Slack message, update a data warehouse. Manually polling your API is slow and wasteful. Outbound webhooks solve it: Velocity X pushes events to customer-owned endpoints the moment something happens.
The catch? Webhooks are async and unreliable by design. Your customer's endpoint might be down, slow, or flaky. If you send once and move on, events disappear into a black hole. Velocity X pattern: signed payloads, exponential backoff retries, and a delivery dashboard so customers see exactly what succeeded and what failed.
Why Outbound Webhooks Matter
No polling tax. Customers stop hitting your API every 30 seconds asking "did anything change?" Instead, you push events the moment they occur. Lower latency, lower bandwidth, happier customers.
Composability. Your app becomes a node in a larger network. Connect to Zapier, build custom pipelines, trigger third-party actions—without you touching the integration layer. Customers own their own extensions.
Async-first architecture. Events are decoupled from your main request cycle. A webhook delivery failing doesn't block a deal from closing or a booking from confirming. Retry logic runs silently in the background, not in the critical path.
Architecture: How Velocity X Sends Webhooks
Admin logs into the dashboard, navigates to Settings → Webhooks → Outbound, and registers an endpoint URL. Each endpoint specifies which event types to subscribe to:
-- webhook_endpoints table
CREATE TABLE webhook_endpoints (
id UUID PRIMARY KEY,
account_id UUID NOT NULL REFERENCES accounts(id),
url TEXT NOT NULL,
secret_key TEXT NOT NULL, -- for HMAC signing
events TEXT[] DEFAULT ARRAY['deal.won', 'booking.confirmed', 'lead.created'],
active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW()
);
-- webhook_events table (outbound queue)
CREATE TABLE webhook_events (
id UUID PRIMARY KEY,
endpoint_id UUID NOT NULL REFERENCES webhook_endpoints(id),
event_type TEXT NOT NULL, -- deal.won
payload JSONB NOT NULL,
status TEXT DEFAULT 'pending', -- pending, delivered, failed, dead-letter
delivered_at TIMESTAMP,
failed_attempts INT DEFAULT 0,
next_retry_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
When a deal closes, an internal event fires: deal.won. Velocity X queries all active endpoints subscribed to that event type and enqueues a delivery job for each. The background job (runs every 10 seconds) picks up pending events and POSTs the payload.
Signing and Security: HMAC Signatures
Webhooks must be cryptographically signed. Your customer's endpoint receives an event, but how do they know it came from Velocity X and not an attacker spoofing your domain? Answer: HMAC-SHA256.
-- Payload POSTed to customer endpoint
POST https://customer.example.com/webhooks/velocity-x
Content-Type: application/json
X-Velocity-Signature: sha256=a1b2c3d4e5f6g7h8...
X-Velocity-Timestamp: 1717939211
{
"id": "evt_deal_123",
"type": "deal.won",
"timestamp": 1717939211,
"data": {
"deal_id": "deal_123",
"deal_name": "Acme Corp—$50K annual",
"closed_by": "alice@company.com"
}
}
-- Customer verifies signature
const timestamp = req.headers['x-velocity-timestamp'];
const signature = req.headers['x-velocity-signature'];
const secret = process.env.VELOCITY_X_WEBHOOK_SECRET;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${JSON.stringify(req.body)}`)
.digest('hex');
if (!crypto.timingSafeEqual(signature, expected)) {
return res.status(401).send('Signature mismatch');
}
// Payload is verified. Process it.
db.deals.log({ id: req.body.data.deal_id, status: 'won' });
Customer stores the webhook secret (shown once in the dashboard, never logged) and uses it to verify every incoming event. No verification? Endpoint rejects it. Velocity X is the only system that can create valid signatures.
Retries and Exponential Backoff
Customer endpoint returns 500. Velocity X doesn't panic. It retries: 10 seconds, 1 minute, 10 minutes, 1 hour, 24 hours. That's 5 attempts over a day. A temporary outage (deploy, traffic spike, database hiccup) is absorbed without data loss. Customer sees failed deliveries in the dashboard with timestamps and error messages.
After 5 failures, the event moves to status = 'dead-letter'. It stops retrying, but it's not lost—it lives in the dashboard forever with full context. Customer can manually review why it failed, fix their endpoint, and trigger a replay button to resend.
Failed Delivery Dashboard
Transparency is king. The customer logs into their account and sees Settings → Webhooks → Delivery Status. A searchable table shows every outbound event: timestamp, event type, endpoint URL, HTTP status code, error message (if any), and delivery latency. Green checkmark = success. Red X = failed. Click to expand and see the full payload and response body.
Filter by event type, date range, or delivery status. Export CSV for auditing. Pin problem endpoints to the top of the view. If a customer says "we didn't get notified when the deal closed," you tell them: "Check your Delivery Status dashboard—event went out at 14:23 UTC, got a 503 timeout, retrying tomorrow." Debugging webhook issues becomes trivial.
Frequently Asked Questions
What if a webhook payload is huge or has sensitive data?
Keep payloads lean. Send event metadata (IDs, timestamps, change type) but not the full object. Customer queries your API with the ID to fetch details if they need them. This reduces payload size, limits exposure of sensitive fields, and forces idempotent consumers—customer's code is simpler.
Can webhooks be replayed manually?
Yes. In the dashboard, dead-letter or delivered events show a "Replay" button. Click it, the event re-enters the queue, and goes back into the retry cycle. Useful after a customer fixes a broken endpoint or after you update your webhook schema.
Should I support webhook filtering or just send all events?
Support filtering by event type at registration time (customer checks boxes: "send me deal.won and booking.confirmed"). Don't support per-field filtering or custom rules—that's scope creep. Simple, predictable event types beat a query language nobody understands.
How do I prevent webhook storms?
Rate-limit per endpoint: max 100 events per second. If you cross the threshold, queue them and space them out. Also monitor customer endpoints: if one endpoint fails 10 times in a row, silence it for 5 minutes to avoid hammering a downed service. Resume silently when the next retry window opens.
What if my customer's endpoint takes 30 seconds to respond?
Set a global timeout: 10 seconds. If the customer's server doesn't respond in that window, treat it as a timeout error and retry. Don't leave the job hanging. This keeps the background queue moving and prevents resource exhaustion.
The Bottom Line
Outbound webhooks let your customers extend your app without you writing integration code. Signing with HMAC keeps payloads secure. Retries with exponential backoff absorb transient failures. A delivery dashboard gives visibility into every event. Velocity X uses this pattern for deal closures, booking confirmations, and lead creation; customers are wiring workflows without asking for API access. If you're building a platform, outbound webhooks are table stakes. See pricing for webhook volume limits and dashboard features, or revisit receiving webhooks at scale.