Skip to main content

Overview

Webhooks allow you to receive real-time notifications when specific events occur in your TitanX account. Instead of polling our API for changes, webhooks push data to your server as events happen, enabling you to build responsive, event-driven integrations.
Understanding Webhook Documentation:
  • Webhook Management API - Endpoints you call to create, list, update, and delete webhook subscriptions
  • Webhook Callbacks - Documentation of the events TitanX sends to your webhook endpoints
  • This guide - Step-by-step instructions for implementing webhooks

How Webhooks Work

  1. Configure a webhook - Set up an HTTPS endpoint URL and subscribe to specific event types
  2. Events occur - When an event happens (e.g., a contact is scored), TitanX creates an event
  3. TitanX sends a POST request - We send the event data to your webhook URL with an HMAC signature
  4. Your server responds - Your endpoint processes the event and returns a 2xx status code
  5. Acknowledgment - We consider the delivery successful when we receive a 2xx response

Supported Events

job.contact.scored

Fired when a contact completes the scoring process. This event provides the full contact object with all phone numbers annotated with their propensity scores. When it fires:
  • After a contact submitted via the scoring API has been fully processed
  • Fires for all contacts processed by a user including contacts processed by both the API and TitanX App
  • Only for contacts belonging to the authenticated user

Webhook Payload Structure

All webhook events follow a consistent structure:
{
  "payload": {
    // Event-specific data (Contact object for job.contact.scored)
  },
  "eventType": "job.contact.scored",
  "timestamp": 1705318200000,
  "apiVersion": "v2",
  "id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
}

Fields

  • payload - The event data - contains the full Contact object with all available fields for job.contact.scored events (see ContactWebhookPayload schema for complete field list)
  • eventType - The type of event (such as “job.contact.scored”)
  • timestamp - Unix timestamp in milliseconds when the event occurred
  • apiVersion - API version (currently “v2”)
  • id - Unique identifier for this webhook event (use for idempotency)

Example: job.contact.scored Payload

The payload contains the complete Contact object with all fields. Here’s an example with commonly used fields:
{
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "userId": "abc123-def456-ghi789",
    "jobId": "job-uuid-here",
    "clientId": "client-uuid-here",
    "firstName": "John",
    "lastName": "Doe",
    "email": "[email protected]",
    "phone": "+15551234567",
    "phoneStatus": "p1",
    "phoneType": "mobile",
    "phoneCountry": "US",
    "phone2": "+15559876543",
    "phone2Status": "p2",
    "phone2Type": "direct",
    "phone2Country": "US",
    "title": "VP of Sales",
    "companyAccount": "Acme Corp",
    "website": "https://acme.com",
    "linkedInUrl": "https://linkedin.com/in/johndoe",
    "companyLinkedInUrl": "https://linkedin.com/company/acme",
    "city": "San Francisco",
    "stateProvince": "CA",
    "country": "USA",
    "companyCity": "San Francisco",
    "companyState": "CA",
    "companyCountry": "USA",
    "numberOfEmployees": "501-1000",
    "annualRevenue": "$50M-$100M",
    "industry": "Technology",
    "leadStatus": "P1",
    "createdAt": "2024-01-15T10:30:00Z",
    "updatedAt": "2024-01-15T10:35:00Z"
  },
  "eventType": "job.contact.scored",
  "timestamp": 1705318200000,
  "apiVersion": "v2",
  "id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
}
The complete Contact object includes 60+ fields covering contact details, up to 5 phone numbers with status/type/country, address information, company firmographics, and more. See the ContactWebhookPayload schema in the API Reference for the exhaustive field list.

Security: Verifying Webhook Signatures

Every webhook request includes an X-TitanX-Signature header containing an HMAC-SHA256 signature. You must verify this signature to ensure the request came from TitanX and hasn’t been tampered with.

Signature Format

The signature is computed as:
Base64(HMAC-SHA256(webhook_secret, request_body))

Verification Example (Node.js)

const crypto = require('crypto');
const express = require('express');

const app = express();

// Important: Get raw body for signature verification
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
  }
}));

app.post('/webhooks/titanx', (req, res) => {
  const signature = req.headers['x-titanx-signature'];
  const webhookSecret = process.env.TITANX_WEBHOOK_SECRET;

  // Compute expected signature
  const hmac = crypto.createHmac('sha256', webhookSecret);
  const expectedSignature = hmac.update(req.rawBody).digest('base64');

  // Verify signature
  if (signature !== expectedSignature) {
    console.error('Invalid webhook signature');
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Signature verified - process the event
  const { eventType, payload, timestamp, id } = req.body;

  console.log(`Received ${eventType} event:`, {
    id,
    timestamp,
    contactId: payload.id
  });

  if (eventType === 'job.contact.scored') {
    // Process scored contact
    console.log('Contact scored:', {
      name: `${payload.firstName} ${payload.lastName}`,
      leadStatus: payload.leadStatus,
      company: payload.companyAccount
    });

    // Your business logic here
    // e.g., update CRM, send notification, trigger workflow
  }

  // Return 200 to acknowledge receipt
  res.status(200).json({ received: true });
});

app.listen(3000, () => {
  console.log('Webhook receiver running on port 3000');
});

Verification Example (Python)

import hmac
import hashlib
import base64
from flask import Flask, request, jsonify

app = Flask(__name__)

WEBHOOK_SECRET = 'your-webhook-secret-here'

@app.route('/webhooks/titanx', methods=['POST'])
def handle_webhook():
    # Get signature from header
    signature = request.headers.get('X-TitanX-Signature')

    # Get raw request body
    raw_body = request.get_data(as_text=True)

    # Compute expected signature
    expected_signature = base64.b64encode(
        hmac.new(
            WEBHOOK_SECRET.encode('utf-8'),
            raw_body.encode('utf-8'),
            hashlib.sha256
        ).digest()
    ).decode('utf-8')

    # Verify signature
    if signature != expected_signature:
        return jsonify({'error': 'Invalid signature'}), 401

    # Parse event
    event = request.json
    event_type = event['eventType']
    payload = event['payload']

    print(f"Received {event_type} event")

    if event_type == 'job.contact.scored':
        # Process scored contact
        print(f"Contact scored: {payload['firstName']} {payload['lastName']}")
        # Your business logic here

    return jsonify({'received': True}), 200

if __name__ == '__main__':
    app.run(port=3000)

Setting Up Webhooks

Step 1: Create Your Webhook Endpoint

Create an HTTPS endpoint that:
  • Accepts POST requests
  • Verifies the X-TitanX-Signature header
  • Returns a 2xx status code within 10 seconds
  • Handles events idempotently (using the event id field)
For detailed information about the webhook callback format and fields, see the Webhook Callbacks documentation.

Step 2: Create the Webhook via API

Use the Create Webhook endpoint from the Webhook Management API:
curl -X POST https://app.titanx.io/api/public/v2/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production Contact Webhook",
    "url": "https://api.example.com/webhooks/titanx",
    "events": ["job.contact.scored"],
    "status": "active"
  }'
Important: The response includes the webhook secret which is only shown once. Store it securely - you’ll need it to verify webhook signatures.

Step 3: Test Your Webhook

After creating your webhook, score some contacts via the API and verify your endpoint receives the events.

Rate Limiting

Webhooks are subject to rate limiting to prevent overwhelming your servers:
  • Default rate limit: 5 requests per second per user
  • Configurable: Rate limits can be adjusted via entitlements
  • Flow control: Webhooks for the same user and event type share a rate limit key
If you need higher rate limits, contact support.

Best Practices

1. Respond Quickly

Return a 2xx status code as soon as you receive the webhook. Process the event asynchronously if needed:
app.post('/webhooks/titanx', async (req, res) => {
  // Verify signature first
  if (!verifySignature(req)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Acknowledge immediately
  res.status(200).json({ received: true });

  // Process asynchronously
  processWebhookAsync(req.body).catch(err => {
    console.error('Error processing webhook:', err);
  });
});

2. Implement Idempotency

Use the event id to prevent processing duplicate events:
const processedEvents = new Set(); // In production, use a database

app.post('/webhooks/titanx', (req, res) => {
  const { id, eventType, payload } = req.body;

  // Check if already processed
  if (processedEvents.has(id)) {
    console.log(`Event ${id} already processed, skipping`);
    return res.status(200).json({ received: true });
  }

  // Process event
  processEvent(eventType, payload);

  // Mark as processed
  processedEvents.add(id);

  res.status(200).json({ received: true });
});

3. Handle Errors Gracefully

If your endpoint returns a non-2xx status code, we’ll consider the delivery failed. Implement retry logic in your application for transient failures.

4. Monitor Webhook Health

  • Log all webhook events for debugging
  • Monitor success/failure rates
  • Set up alerts for consistent failures
  • Use the webhook status endpoint to pause webhooks during maintenance

5. Use HTTPS

Webhook URLs must use HTTPS. This ensures:
  • Data is encrypted in transit
  • Your endpoint is authenticated
  • Man-in-the-middle attacks are prevented

Managing Webhooks

List All Webhooks

curl https://app.titanx.io/api/public/v2/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY"

Update Webhook Status

Pause a webhook without deleting it:
curl -X POST https://app.titanx.io/api/public/v2/webhooks/{id}/inactive \
  -H "Authorization: Bearer YOUR_API_KEY"
Reactivate:
curl -X POST https://app.titanx.io/api/public/v2/webhooks/{id}/active \
  -H "Authorization: Bearer YOUR_API_KEY"

Delete a Webhook

curl -X DELETE https://app.titanx.io/api/public/v2/webhooks/{id} \
  -H "Authorization: Bearer YOUR_API_KEY"

Understanding Contact Scores

When you receive a job.contact.scored event, the contact will include phone status annotations:

Phone Statuses

phoneStatus
enum
The status of the phone number
IVR stands for Interactive Voice Response, which you may know as a dial tree

Lead Status

Each contact has an overall leadStatus field that represents the highest scoring phone number:
leadStatus
enum
The overall status of the contact based on the highest scoring phone status.

Troubleshooting

Webhook Not Firing

  • Verify the webhook status is “active”
  • Ensure events are from API-originated contacts (not app submissions)
  • Check that contacts belong to the authenticated user
  • Verify your endpoint is accessible from the internet

Signature Verification Failing

  • Ensure you’re using the raw request body (not parsed JSON)
  • Verify you’re using the correct webhook secret
  • Check that you’re computing Base64-encoded HMAC-SHA256
  • Verify the signature header name is exactly X-TitanX-Signature

Timeouts

  • Return a 2xx response within 10 seconds
  • Process events asynchronously if they take longer
  • Consider using a message queue for complex processing

Need Help?

If you have questions or need assistance with webhooks: