Developer Documentation

Webhook API

Integrate with Contacts4us webhooks to receive real-time notifications when contacts are created, updated, or deleted.

API Version 2026-03-01
Format JSON
Auth HMAC-SHA256

Webhook API Contract

Overview

Contacts4Us sends outbound webhooks to notify external systems when contact-related events occur. This document defines the API contract that webhook consumers must implement to receive these notifications.


Webhook Configuration

Multiple Endpoints

Users and organizations can configure multiple webhook URLs. Each endpoint:

  • Has its own name for identification in the UI
  • Has its own HMAC secret for signature verification
  • Can be enabled or disabled independently
  • Tracks delivery status separately

URL Requirements

  • Must use HTTPS (HTTP is rejected)
  • Must be reachable from Contacts4Us servers
  • Should respond within 30 seconds

Event Types

| Event | Description | When Fired | |-------|-------------|------------| | contact.created | A new contact was added | After card scan confirmation or manual entry | | contact.updated | An existing contact was modified | After contact edit is saved | | contact.deleted | A contact was removed | After contact deletion | | contact.batch_created | Multiple contacts were added at once | After batch scan or import | | profile.updated | A profile owner updated their info | When owner edits claimed profile (sent to all who have this profile) |


Payload Structure

Envelope

All webhook payloads share this top-level structure:

| Field | Type | Description | |-------|------|-------------| | event_type | string | One of the event types listed above | | event_id | string (UUID) | Unique identifier for this event (for idempotency) | | timestamp | string (ISO 8601) | When the event occurred, in UTC | | api_version | string | API version (currently 2026-03-01) | | source | string | Always contacts4us | | account | object | Information about the account that owns this data | | data | object | Event-specific payload |

Account Object

| Field | Type | Description | |-------|------|-------------| | user_id | string (UUID) | The user who owns these contacts | | user_email | string | User's email address | | organization_id | string (UUID) or null | Organization ID if this is an org contact | | organization_name | string or null | Organization name if applicable |

Contact Object

Used within the data field for contact events:

| Field | Type | Nullable | Description | |-------|------|----------|-------------| | id | string (UUID) | no | Contacts4Us internal ID | | first_name | string | yes | | | last_name | string | yes | | | email | string | yes | | | phones | array of objects | no | All phone numbers (see Phone Object below) | | title | string | yes | Job title | | company_name | string | yes | | | website | string | yes | | | address | object | yes | Structured address (see below) | | field | string | yes | Professional field category | | notes | string | yes | User-added notes | | tags | array of strings | no | Tag names applied to this contact | | created_at | string (ISO 8601) | no | When contact was first added | | updated_at | string (ISO 8601) | no | When contact was last modified | | scanned_at | string (ISO 8601) | yes | When the business card was scanned (null if manually entered) |

Phone Object

| Field | Type | Description | |-------|------|-------------| | number | string | The phone number | | type | string | One of: cell, work, home, fax, other | | is_primary | boolean | Whether this is the primary phone |

Address Object

| Field | Type | Description | |-------|------|-------------| | line_1 | string or null | Street address | | line_2 | string or null | Apt, suite, etc. | | city | string or null | | | state | string or null | State or province | | postal_code | string or null | ZIP or postal code | | country | string or null | Country name |


Example Payloads

contact.created

{
"event_type": "contact.created",
"event_id": "evt_01234567-89ab-cdef-0123-456789abcdef",
"timestamp": "2026-03-15T14:30:00Z",
"api_version": "2026-03-01",
"source": "contacts4us",
"account": {
"user_id": "usr_fedcba98-7654-3210-fedc-ba9876543210",
"user_email": "[email protected]",
"organization_id": "org_11111111-2222-3333-4444-555555555555",
"organization_name": "Acme Corp"
},
"data": {
"contact": {
"id": "con_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"first_name": "Jane",
"last_name": "Smith",
"email": "[email protected]",
"phones": [
{ "number": "+1-555-123-4567", "type": "cell", "is_primary": true },
{ "number": "+1-555-987-6543", "type": "work", "is_primary": false }
],
"title": "VP of Engineering",
"company_name": "TechCo Industries",
"website": "https://techco.com",
"address": {
"line_1": "123 Innovation Drive",
"line_2": "Suite 400",
"city": "San Francisco",
"state": "CA",
"postal_code": "94105",
"country": "United States"
},
"field": "Technology",
"notes": null,
"tags": ["prospect", "conference-lead"],
"created_at": "2026-03-15T14:30:00Z",
"updated_at": "2026-03-15T14:30:00Z",
"scanned_at": "2026-03-15T14:29:45Z"
}
}
}

contact.updated

{
"event_type": "contact.updated",
"event_id": "evt_99999999-8888-7777-6666-555555555555",
"timestamp": "2026-03-16T09:15:00Z",
"api_version": "2026-03-01",
"source": "contacts4us",
"account": {
"user_id": "usr_fedcba98-7654-3210-fedc-ba9876543210",
"user_email": "[email protected]",
"organization_id": null,
"organization_name": null
},
"data": {
"contact": {
"id": "con_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"first_name": "Jane",
"last_name": "Smith",
"email": "[email protected]",
"phones": [
{ "number": "+1-555-123-4567", "type": "cell", "is_primary": true }
],
"title": "CTO",
"company_name": "TechCo Industries",
"website": "https://techco.com",
"address": {
"line_1": "123 Innovation Drive",
"line_2": "Suite 400",
"city": "San Francisco",
"state": "CA",
"postal_code": "94105",
"country": "United States"
},
"field": "Technology",
"notes": "Promoted in Mar 2026",
"tags": ["prospect", "conference-lead", "decision-maker"],
"created_at": "2026-03-15T14:30:00Z",
"updated_at": "2026-03-16T09:15:00Z",
"scanned_at": "2026-03-15T14:29:45Z"
},
"changes": {
"title": {
"old": "VP of Engineering",
"new": "CTO"
},
"notes": {
"old": null,
"new": "Promoted in Mar 2026"
},
"tags": {
"added": ["decision-maker"],
"removed": []
}
}
}
}

contact.deleted

{
"event_type": "contact.deleted",
"event_id": "evt_delete12-3456-7890-abcd-ef1234567890",
"timestamp": "2026-03-17T16:00:00Z",
"api_version": "2026-03-01",
"source": "contacts4us",
"account": {
"user_id": "usr_fedcba98-7654-3210-fedc-ba9876543210",
"user_email": "[email protected]",
"organization_id": null,
"organization_name": null
},
"data": {
"contact_id": "con_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"deleted_at": "2026-03-17T16:00:00Z"
}
}

contact.batch_created

{
"event_type": "contact.batch_created",
"event_id": "evt_batch123-4567-8901-2345-678901234567",
"timestamp": "2026-03-15T15:00:00Z",
"api_version": "2026-03-01",
"source": "contacts4us",
"account": {
"user_id": "usr_fedcba98-7654-3210-fedc-ba9876543210",
"user_email": "[email protected]",
"organization_id": "org_11111111-2222-3333-4444-555555555555",
"organization_name": "Acme Corp"
},
"data": {
"contacts": [
{
"id": "con_11111111-1111-1111-1111-111111111111",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phones": [],
"title": "Sales Manager",
"company_name": "Example Inc",
"website": null,
"address": null,
"field": "Retail",
"notes": null,
"tags": [],
"created_at": "2026-03-15T15:00:00Z",
"updated_at": "2026-03-15T15:00:00Z",
"scanned_at": "2026-03-15T14:58:00Z"
},
{
"id": "con_22222222-2222-2222-2222-222222222222",
"first_name": "Sarah",
"last_name": "Johnson",
"email": "[email protected]",
"phones": [
{ "number": "+1-555-987-6543", "type": "work", "is_primary": true }
],
"title": "Director",
"company_name": "Company LLC",
"website": "https://company.com",
"address": null,
"field": "Consulting",
"notes": null,
"tags": [],
"created_at": "2026-03-15T15:00:00Z",
"updated_at": "2026-03-15T15:00:00Z",
"scanned_at": "2026-03-15T14:59:00Z"
}
],
"batch_size": 2
}
}

profile.updated

Sent when a profile owner updates their own information. This goes to all users who have this profile as a contact (if they have sync_updates enabled).

{
"event_type": "profile.updated",
"event_id": "evt_profile1-2345-6789-0abc-def012345678",
"timestamp": "2026-03-20T10:00:00Z",
"api_version": "2026-03-01",
"source": "contacts4us",
"account": {
"user_id": "usr_fedcba98-7654-3210-fedc-ba9876543210",
"user_email": "[email protected]",
"organization_id": null,
"organization_name": null
},
"data": {
"contact": {
"id": "con_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"first_name": "Jane",
"last_name": "Smith",
"email": "[email protected]",
"phones": [
{ "number": "+1-555-999-8888", "type": "cell", "is_primary": true }
],
"title": "CTO",
"company_name": "TechCo Industries",
"website": "https://techco.com",
"address": {
"line_1": "456 New Office Blvd",
"line_2": null,
"city": "Austin",
"state": "TX",
"postal_code": "78701",
"country": "United States"
},
"field": "Technology",
"notes": "Promoted in Mar 2026",
"tags": ["prospect", "conference-lead", "decision-maker"],
"created_at": "2026-03-15T14:30:00Z",
"updated_at": "2026-03-20T10:00:00Z",
"scanned_at": "2026-03-15T14:29:45Z"
},
"changes": {
"phones": {
"old": [
{ "number": "+1-555-123-4567", "type": "cell", "is_primary": true }
],
"new": [
{ "number": "+1-555-999-8888", "type": "cell", "is_primary": true }
]
},
"address": {
"old": {
"line_1": "123 Innovation Drive",
"line_2": "Suite 400",
"city": "San Francisco",
"state": "CA",
"postal_code": "94105",
"country": "United States"
},
"new": {
"line_1": "456 New Office Blvd",
"line_2": null,
"city": "Austin",
"state": "TX",
"postal_code": "78701",
"country": "United States"
}
}
},
"updated_by": "profile_owner"
}
}

Authentication

HMAC Signature

All webhook requests include an HMAC-SHA256 signature for verification. Each webhook URL has its own secret, configured when the webhook is created.

Signature Headers

| Header | Description | |--------|-------------| | X-Contacts4us-Signature | HMAC-SHA256 signature of the request body | | X-Contacts4us-Timestamp | Unix timestamp when signature was generated |

Signature Calculation

The signature is calculated as:

HMAC-SHA256(webhook_secret, timestamp + "." + request_body)

Where:

  • webhook_secret is the secret configured for this webhook URL
  • timestamp is the Unix timestamp (same as X-Contacts4us-Timestamp header)
  • request_body is the raw JSON payload (before any parsing)

Verification Steps

  1. Extract the timestamp from X-Contacts4us-Timestamp header
  2. Verify the timestamp is within 5 minutes of current time (prevents replay attacks)
  3. Concatenate: timestamp + "." + raw_request_body
  4. Compute HMAC-SHA256 using your webhook secret
  5. Compare your computed signature with X-Contacts4us-Signature header
  6. Use constant-time comparison to prevent timing attacks

Verification Example (Elixir)

def verify_signature(payload, timestamp, signature, secret) do
# Check timestamp freshness (within 5 minutes)
current_time = System.system_time(:second)
timestamp_int = String.to_integer(timestamp)
if abs(current_time - timestamp_int) > 300 do
{:error, :timestamp_expired}
else
# Compute expected signature
signed_payload = timestamp <> "." <> payload
expected = :crypto.mac(:hmac, :sha256, secret, signed_payload)
|> Base.encode16(case: :lower)
# Constant-time comparison
if Plug.Crypto.secure_compare(expected, signature) do
:ok
else
{:error, :invalid_signature}
end
end
end

Verification Example (Node.js)

const crypto = require('crypto');
function verifySignature(payload, timestamp, signature, secret) {
// Check timestamp freshness (within 5 minutes)
const currentTime = Math.floor(Date.now() / 1000);
const timestampInt = parseInt(timestamp, 10);
if (Math.abs(currentTime - timestampInt) > 300) {
return { valid: false, error: 'timestamp_expired' };
}
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Constant-time comparison
const valid = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
return { valid };
}

Request Details

HTTP Method

All webhooks are sent as POST requests.

Headers

| Header | Value | |--------|-------| | Content-Type | application/json | | User-Agent | Contacts4us-Webhook/1.0 | | X-Contacts4us-Signature | HMAC signature | | X-Contacts4us-Timestamp | Unix timestamp | | X-Contacts4us-Event | Event type (e.g., contact.created) | | X-Contacts4us-Delivery-Id | Unique ID for this delivery attempt |

Timeout

Webhook requests timeout after 30 seconds. If your endpoint needs more time to process, return a 2xx response immediately and process asynchronously.


Expected Responses

Success

Return any 2xx status code to indicate successful receipt:

| Status | Meaning | |--------|---------| | 200 OK | Success | | 201 Created | Success (resource created) | | 202 Accepted | Success (will process asynchronously) | | 204 No Content | Success (no response body) |

Response body is optional and ignored.

Failure

Any non-2xx response is treated as a failure and triggers retry logic:

| Status | Behavior | |--------|----------| | 3xx | Treated as failure (redirects not followed) | | 4xx | Treated as failure, retried (may indicate temporary auth issue) | | 5xx | Treated as failure, retried | | Timeout | Treated as failure, retried | | Connection error | Treated as failure, retried |


Retry Behavior

Retry Schedule

Failed deliveries are retried with exponential backoff:

| Attempt | Delay After Failure | |---------|---------------------| | 1 | Immediate | | 2 | 30 seconds | | 3 | 5 minutes | | 4 | 30 minutes |

After 4 failed attempts, the delivery is marked as permanently failed.

Retry Tracking

Each webhook URL tracks failures independently. If you have two webhook URLs configured:

  • URL A succeeds immediately
  • URL B fails and retries

URL A's delivery is complete, while URL B continues retrying.

Manual Retry

Users can manually trigger a retry for failed deliveries from the Contacts4Us UI. This:

  • Resets the attempt counter
  • Sends the original payload immediately
  • Follows the same retry schedule if it fails again

Idempotency

Event IDs

Each event has a unique event_id. Use this to prevent duplicate processing:

  1. When you receive a webhook, check if you've already processed this event_id
  2. If yes, return 2xx but skip processing
  3. If no, process the webhook and store the event_id

Why Duplicates Occur

Duplicates can occur when:

  • Network issues cause us to not receive your 2xx response
  • Manual retry is triggered
  • System failover during delivery

Always implement idempotency checks.


Testing Webhooks

Test Endpoint

Users can send a test webhook from the Contacts4Us UI. Test webhooks:

  • Use event type test.ping
  • Contain minimal sample data
  • Verify connectivity and signature validation

Test Payload

{
"event_type": "test.ping",
"event_id": "evt_test0000-0000-0000-0000-000000000000",
"timestamp": "2026-03-15T12:00:00Z",
"api_version": "2026-03-01",
"source": "contacts4us",
"account": {
"user_id": "usr_fedcba98-7654-3210-fedc-ba9876543210",
"user_email": "[email protected]",
"organization_id": null,
"organization_name": null
},
"data": {
"message": "This is a test webhook. If you received this, your endpoint is configured correctly."
}
}

Local Development

For local testing, use a tunneling service like ngrok to expose your local endpoint:

  1. Start your local webhook handler
  2. Run ngrok: ngrok http 3000
  3. Configure the ngrok URL as your webhook endpoint in Contacts4Us
  4. Send a test webhook

Disabling Webhooks

Per-Contact Control

Each contact relationship has a webhook_sync_enabled flag. When disabled:

  • Changes to that contact do not trigger webhooks
  • Useful for contacts you manage elsewhere

Per-Webhook Control

Each webhook URL can be disabled without deletion:

  • Disabled webhooks receive no events
  • Re-enabling resumes delivery (does not replay missed events)

Automatic Disabling

A webhook URL is automatically disabled after:

  • 10 consecutive failures within 24 hours
  • Users are notified via email when this occurs
  • Manual re-enabling is required

Rate Limits

Outbound Limits

Contacts4Us limits outbound webhooks to prevent overwhelming your endpoints:

| Limit | Value | |-------|-------| | Per webhook URL | 100 requests/minute | | Per account (all webhooks) | 500 requests/minute |

If limits are exceeded, deliveries are queued and sent when capacity is available.

Your Endpoint

Your endpoint should be able to handle bursts during:

  • Batch imports
  • Multi-card scans
  • Profile propagation updates (one contact owner updating affects many recipients)

Versioning

API Version Header

All payloads include api_version indicating the contract version.

Breaking Changes

We will:

  • Announce breaking changes 90 days in advance
  • Support old versions for 6 months after new version release
  • Allow version selection per webhook URL (future feature)

Non-Breaking Changes

We may add new fields to payloads at any time. Your integration should:

  • Ignore unknown fields
  • Not fail if new fields are present

Security Recommendations

  1. Always verify signatures - Never process webhooks without signature verification
  2. Use HTTPS - All webhook URLs must use HTTPS
  3. Validate timestamps - Reject webhooks with old timestamps
  4. Implement idempotency - Handle duplicate deliveries gracefully
  5. Respond quickly - Return 2xx within 30 seconds, process async if needed
  6. Rotate secrets - Periodically rotate webhook secrets
  7. Monitor failures - Set up alerts for failed webhook deliveries

Support

For webhook integration issues:

  • Check the delivery logs in your Contacts4Us dashboard
  • Review the response code and body from your endpoint
  • Contact support with the X-Contacts4us-Delivery-Id for specific delivery issues