Events API Guide

Events API Guide

The Events API allows you to track all interactions and activities with your contacts. Events are the foundation of engagement tracking in Sure Send CRM, capturing everything from page views to form submissions to property searches.

Overview

Events represent actions or interactions that occur with your contacts. They help you:

  • Build engagement timelines
  • Track website activity
  • Monitor property interest
  • Measure campaign effectiveness
  • Trigger automations based on behavior

Common Event Types:

Send as typeRead back as canonicalEventTypeMeaning
page_viewViewed PageWebsite page viewed
property_viewViewed PropertyProperty listing viewed
property_saveSaved PropertyProperty saved to favorites
property_searchProperty SearchProperty search performed
saved_property_searchSaved Property SearchSearch saved for alerts
form_submissionForm SubmissionContact form submitted
property_inquiryProperty InquiryInquiry about a specific property
seller_inquirySeller InquirySeller or home valuation inquiry
general_inquiryGeneral InquiryDirect inquiry submitted
registrationRegistrationAccount or portal registration
incoming_callIncoming CallInbound phone call
visited_open_houseVisited Open HouseOpen house attendance

Custom types are fully supported. Send anything you need — email_opened, email_clicked,
appointment_scheduled, document_downloaded — and it is stored and returned exactly as you
wrote it.

📘

Sending an event type vs. matching on one

The type you send is stored verbatim and returned as eventType. Every event also carries
canonicalEventType, a normalized value that folds all spellings of the same concept together.
When you read events back, match on canonicalEventType — see
Matching Event Types.

Creating Events

Basic Event

Endpoint: POST /api/partner/events

curl -X POST https://api.suresend.ai/api/partner/events \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "page_view",
    "personId": "person-uuid",
    "pageUrl": "https://example.com/about",
    "pageTitle": "About Us"
  }'

Response:

{
  "person": { "id": "person-uuid", "firstName": "John", "lastName": "Doe" },
  "event": {
    "id": "event-uuid",
    "type": "page_view",
    "canonicalEventType": "Viewed Page",
    "personId": "person-uuid",
    "occurredAt": "2025-10-16T12:00:00Z",
    "created": "2025-10-16T12:00:00Z"
  },
  "wasPersonCreated": false,
  "wasCompanyCreated": false
}
📘

The create response nests the event under event and names the raw type type (the list and

detail endpoints name the same value eventType). canonicalEventType is spelled the same way
everywhere.

Page View Event

Track when contacts visit pages on your website:

curl -X POST https://api.suresend.ai/api/partner/events \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "page_view",
    "personId": "person-uuid",
    "pageUrl": "https://example.com/properties/123",
    "pageTitle": "Beautiful 3BR Home",
    "pageDuration": 45,
    "pageReferrer": "https://google.com/search",
    "ipAddress": "192.168.1.1",
    "userAgent": "Mozilla/5.0..."
  }'

Property View Event

Track interest in specific properties (real estate):

curl -X POST https://api.suresend.ai/api/partner/events \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "property_view",
    "personId": "person-uuid",
    "message": "Viewed 3-bedroom home in Portland",
    "property": {
      "id": "prop-123",
      "address": "123 Main St, Portland, OR 97201",
      "price": 500000,
      "beds": 3,
      "baths": 2,
      "sqft": 1800,
      "propertyType": "house",
      "listingUrl": "https://example.com/properties/123"
    }
  }'

Saved Property Event

Track when contacts save properties to their favorites or saved lists:

curl -X POST https://api.suresend.ai/api/partner/events \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "property_save",
    "personId": "person-uuid",
    "message": "Saved property to favorites",
    "description": "Saved 3-bedroom home to favorites list",
    "property": {
      "id": "prop-456",
      "address": "456 Oak Ave, Portland, OR 97202",
      "price": 650000,
      "beds": 3,
      "baths": 2.5,
      "sqft": 2100,
      "propertyType": "house",
      "listingUrl": "https://example.com/properties/456",
      "savedToList": "Favorites"
    },
    "metadata": {
      "savedFrom": "search results",
      "deviceType": "mobile",
      "interestLevel": "very-high",
      "action": "saved_to_favorites"
    }
  }'

Property Data Fields:

  • savedToList - Name of the saved list (e.g., "Favorites", "Shortlist", "Must See")

Metadata Fields:

  • savedFrom - Where the property was saved from (e.g., "search results", "property detail page", "agent recommendation")
  • deviceType - Device used to save the property
  • interestLevel - Indicates high engagement (e.g., "very-high")
  • action - Specific action taken (e.g., "saved_to_favorites", "added_to_shortlist")

Property Search Event

Track search behavior and preferences:

curl -X POST https://api.suresend.ai/api/partner/events \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "property_search",
    "personId": "person-uuid",
    "message": "Searched for homes in Portland",
    "propertySearch": {
      "minPrice": 300000,
      "maxPrice": 500000,
      "beds": 3,
      "baths": 2,
      "location": "Portland, OR",
      "propertyType": "house",
      "keywords": "modern kitchen, garage"
    }
  }'

Form Submission Event

Track form submissions with contact creation:

curl -X POST https://api.suresend.ai/api/partner/events \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "form_submission",
    "person": {
      "firstName": "John",
      "lastName": "Doe",
      "emails": [{"value": "[email protected]", "isPrimary": true}],
      "phones": [{"value": "+1-555-1234", "isPrimary": true}]
    },
    "message": "Contact form submitted",
    "description": "User requested more information about properties",
    "metadata": {
      "formName": "Contact Us",
      "formId": "contact-form-1",
      "interests": ["buying", "3-bedroom"],
      "budget": "300k-500k"
    }
  }'

Response includes person:

{
  "event": { ... },
  "person": {
    "id": "person-uuid",
    "firstName": "John",
    "lastName": "Doe",
    ...
  },
  "wasPersonCreated": true,  // true if a new person was created, false if existing
  "wasCompanyCreated": false
}

Email Campaign Event

Track email engagement:

curl -X POST https://api.suresend.ai/api/partner/events \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "email_opened",
    "personId": "person-uuid",
    "message": "Opened Spring 2025 campaign email",
    "campaign": {
      "name": "Spring 2025 Listings",
      "id": "campaign-123",
      "source": "mailchimp",
      "medium": "email"
    }
  }'

Person Matching

When creating events, you can associate them with people in three ways:

1. Use Existing Person ID

If you already know the person ID:

{
  "type": "page_view",
  "personId": "existing-person-uuid",
  "pageUrl": "https://example.com/page"
}

2. Match Existing Person by Email/Phone

Provide person data, and the system will find the matching contact:

{
  "type": "page_view",
  "person": {
    "emails": [{"value": "[email protected]"}]
  },
  "pageUrl": "https://example.com/page"
}

3. Create New Person Automatically

If no match is found, a new person is created:

{
  "type": "form_submission",
  "person": {
    "firstName": "New",
    "lastName": "Lead",
    "emails": [{"value": "[email protected]", "isPrimary": true}],
    "phones": [{"value": "+1-555-1234", "isPrimary": true}],
    "tags": ["website-lead"]
  },
  "message": "New lead from website"
}

Company Association

When creating events, you can optionally associate a company with the person. This is useful for:

  • B2B lead tracking where contacts represent businesses
  • Linking form submissions to company accounts
  • Automatically associating people with their employers
  • Tracking company-level engagement

How Company Association Works

  1. Find by ID: If you provide a company id, the system looks up that specific company
  2. Find by Name: If no ID, the system searches for an existing company by name (case-insensitive)
  3. Create if Not Found: If no match is found and name is provided, a new company is created
  4. Automatic Person Association: When a company is found or created, the person is automatically linked to that company

Company Parameter Structure

{
  "company": {
    "id": "uuid (optional - use to link to existing company)",
    "name": "string (required if no id)",
    "website": "string",
    "industry": "string",
    "companySize": "string",
    "description": "string",
    "annualRevenue": 5000000,
    "registrationNumber": "string",
    "taxId": "string",
    "primaryEmail": "string",
    "primaryPhone": "string",
    "emails": [
      { "value": "[email protected]", "type": "work", "isPrimary": true }
    ],
    "phones": [
      { "value": "+1-555-1234", "type": "main", "isPrimary": true }
    ],
    "addresses": [
      {
        "street": "123 Business St",
        "city": "San Francisco",
        "state": "CA",
        "code": "94105",
        "country": "US",
        "type": "headquarters",
        "isPrimary": true
      }
    ],
    "tags": ["enterprise", "tech-sector"],
    "customFields": {
      "accountTier": "Gold"
    }
  }
}

Response with Company

When a company is included in the request, the response includes the company object and a creation flag:

{
  "person": { ... },
  "event": { ... },
  "company": {
    "id": "company-uuid",
    "name": "Acme Corporation",
    "website": "https://acme.com",
    "industry": "Technology",
    "companySize": "51-200",
    "description": "Leading provider of innovative solutions",
    "emails": [
      { "value": "[email protected]", "type": "work", "isPrimary": true, "status": "active" }
    ],
    "phones": [
      { "value": "+1-555-1234", "type": "main", "isPrimary": true, "status": "active" }
    ],
    "addresses": [
      {
        "street": "123 Business St",
        "city": "San Francisco",
        "state": "CA",
        "code": "94105",
        "country": "US",
        "type": "headquarters",
        "isPrimary": true
      }
    ],
    "tags": ["enterprise"],
    "created": "2025-10-22T12:00:00Z",
    "updated": "2025-10-22T12:00:00Z"
  },
  "wasPersonCreated": true,
  "wasCompanyCreated": true
}

HTTP Status Codes

The response status code indicates what was created:

  • 201 Created: A new person OR company was created
  • 200 OK: Both person and company already existed (matched existing records)

Example: Form Submission with Company

curl -X POST https://api.suresend.ai/api/partner/events \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "form_submission",
    "person": {
      "firstName": "Jane",
      "lastName": "Smith",
      "emails": [{"value": "[email protected]", "isPrimary": true}],
      "phones": [{"value": "+1-555-2345", "isPrimary": true}]
    },
    "company": {
      "name": "Acme Corporation",
      "website": "https://acmecorp.com",
      "industry": "Technology",
      "companySize": "51-200"
    },
    "message": "Demo request submitted",
    "metadata": {
      "formName": "Demo Request",
      "productInterest": "Enterprise Plan"
    }
  }'

Example: Link to Existing Company by ID

curl -X POST https://api.suresend.ai/api/partner/events \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "inquiry",
    "person": {
      "firstName": "Bob",
      "lastName": "Johnson",
      "emails": [{"value": "[email protected]"}]
    },
    "company": {
      "id": "existing-company-uuid"
    },
    "message": "Sales inquiry from existing account"
  }'

Company Deduplication

The system uses case-insensitive name matching to prevent duplicate companies:

  • "Acme Corporation" matches "acme corporation" and "ACME CORPORATION"
  • Whitespace is trimmed from company names
  • If a match is found, existing company data is updated (non-destructive merge)
  • New emails, phones, addresses, and tags are added without removing existing ones

Person-Company Association

When both person and company are involved:

  • A PersonCompany association is automatically created
  • If the company was newly created, it is set as the person's primary company
  • If the association already exists, no duplicate is created
  • The is_current flag is set to true for new associations

Validation

The company parameter has the following requirements:

  • Company name is required (unless providing an id for lookup)
  • If an id is provided, the company must exist in your team
  • All other fields are optional

Event Data Structure

Core Fields

All events have these base fields:

{
  "type": "page_view",           // Required: Event type
  "personId": "uuid",             // Person ID (use personId OR person)
  "person": {...},                // Person data (use personId OR person)
  "company": {...},               // Company data (optional, see Company Association)
  "occurredAt": "2025-10-16...",  // When event occurred (ISO 8601)
  "message": "Short summary",     // Brief description
  "description": "Long details",  // Detailed description
  "source": "website",            // Event source
  "system": "google_analytics"    // Tracking system
}

Page View Fields

{
  "pageUrl": "https://example.com/page",
  "pageTitle": "Page Title",
  "pageDuration": 45,             // Time on page (seconds)
  "pageReferrer": "https://google.com",
  "ipAddress": "192.168.1.1",
  "userAgent": "Mozilla/5.0..."
}

Property Data

{
  "property": {
    "id": "prop-123",
    "address": "123 Main St",
    "price": 500000,
    "beds": 3,
    "baths": 2,
    "sqft": 1800,
    "propertyType": "house",
    "listingUrl": "https://...",
    // Any custom fields
  }
}

Property Search Data

{
  "propertySearch": {
    "minPrice": 300000,
    "maxPrice": 500000,
    "beds": 3,
    "baths": 2,
    "location": "Portland, OR",
    "propertyType": "house",
    "keywords": "modern, garage"
  }
}

Campaign Data

{
  "campaign": {
    "name": "Spring 2025",
    "id": "campaign-123",
    "source": "facebook",
    "medium": "cpc",
    "term": "portland homes",
    "content": "ad-variant-a"
  }
}

Custom Metadata

The metadata field allows you to store any additional data as flexible key-value pairs. This is perfect for:

  • Form-specific data (form name, form ID, submission details)
  • User preferences and interests
  • Session/tracking information
  • Budget/price preferences
  • Custom attributes specific to your application
  • Integration-specific data

Flexible Structure:

{
  "metadata": {
    "formName": "Contact Form",
    "formId": "contact-form-1",
    "deviceType": "mobile",
    "sessionId": "session-123",
    "interests": ["buying", "3-bedroom", "garage"],
    "budget": "300k-500k",
    "customField": "any value",
    "nestedData": {
      "key": "value",
      "another": "nested value"
    }
  }
}

Benefits:

  • ✅ Store any JSON-serializable data
  • ✅ No schema restrictions
  • ✅ Automatically displayed in CRM activity timeline
  • ✅ Indexed for efficient querying
  • ✅ Supports nested objects and arrays

Limits:
To ensure system performance and prevent abuse, metadata has the following reasonable limits:

  • Maximum Size: 256 KB (262,144 bytes)
    • Plenty of space for detailed form data and tracking information
    • Most metadata will be under 5 KB
  • Maximum Keys: 100 keys at root level
    • Sufficient for comprehensive event tracking
    • Nested objects can have additional keys
  • Maximum Depth: 10 levels of nesting
    • Prevents overly complex structures
    • More than enough for any legitimate use case

Example of Acceptable Metadata:

{
  "formData": {
    "personalInfo": {
      "name": "John Doe",
      "email": "[email protected]",
      "preferences": {
        "budget": "300k-500k",
        "bedrooms": 3,
        "locations": ["Charleston", "Mount Pleasant"]
      }
    }
  },
  "tracking": {
    "referrer": "google.com",
    "sessionId": "abc123",
    "deviceType": "mobile"
  },
  "engagement": {
    "timeOnPage": 120,
    "scrollDepth": 85,
    "clicks": ["cta-button", "property-card"]
  }
}

This example is well within limits (< 1 KB, 15 keys, 4 levels deep).

Matching Event Types

Every event carries two type fields, and they answer different questions.

FieldWhat it isUse it for
eventTypeThe raw value exactly as stored, in whatever format the producer wrote it — Viewed Property, page_view, property_save can all appear in a single responseDisplaying the original value, debugging, round-tripping data you wrote yourself
canonicalEventTypeThe normalized Title Case value. All spellings of one concept collapse to one valueMatching, filtering, grouping, routing — this is the field to build on

Events reach Sure Send from many producers — our tracking pixel, partner integrations, imports, and
your own API calls — and each writes the type its own way. canonicalEventType folds those together
so you never have to guess:

[
  { "eventType": "Viewed Property", "canonicalEventType": "Viewed Property" },
  { "eventType": "property_save",   "canonicalEventType": "Saved Property"  },
  { "eventType": "page_view",       "canonicalEventType": "Viewed Page"     }
]

Canonical values:

Registration · Seller Inquiry · Property Inquiry · General Inquiry · Viewed Page ·
Viewed Property · Saved Property · Incoming Call · Visited Open House · Property Search ·
Saved Property Search · Form Submission

Normalization ignores case, delimiter, and word order, so property_view, Property View,
viewedProperty, VIEWED-PROPERTY and Viewed Property all canonicalize to Viewed Property.
Property views recorded by Lofty (stored as Browse) canonicalize to Viewed Property too.

🚧

Treat this vocabulary as open, not closed

Types outside the list above — including any custom type you send — pass through to
canonicalEventType unchanged rather than being dropped or nulled. The field is always present.
New canonical values may be added over time, so handle unrecognized values gracefully instead of
switching exhaustively.

eventType is a frozen contract: it returns the exact stored bytes and will never be retroactively
normalized, so integrations built against raw values keep working.

Retrieving Events

List All Events

Endpoint: GET /api/partner/events

curl "https://api.suresend.ai/api/partner/events?limit=50" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Query Parameters:

  • personId - Filter by person
  • canonicalEventType - Recommended. Filter by canonical event type; matches every stored
    spelling of that type. Accepts a single value, a comma-separated list, or a repeated array
    parameter (canonicalEventType[]=Viewed Page&canonicalEventType[]=Saved Property)
  • eventType - Filter by the raw stored value. ⚠️ Exact, case-sensitive match — events stored
    under a different spelling of the same concept will not match
  • source - Filter by source
  • limit - Items per page (default: 25, max: 100)
  • page - Page number (default: 1)

Response:

{
  "events": [
    {
      "id": "event-uuid",
      "personId": "person-uuid",
      "eventType": "page_view",
      "canonicalEventType": "Viewed Page",
      "occurredAt": "2025-10-16T12:00:00Z",
      "pageUrl": "https://example.com/page",
      "metadata": {
        "formName": "Contact Us",
        "interests": ["buying"]
      },
      "created": "2025-10-16T12:00:00Z",
      "updated": "2025-10-16T12:00:00Z"
    }
  ],
  "meta": {
    "total_count": 137,
    "current_page": 1,
    "total_pages": 6,
    "per_page": 25
  }
}

Note: The metadata field is included in responses when present on the event.

Filter Events by Person

curl "https://api.suresend.ai/api/partner/events?personId=person-uuid&limit=100" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Filter Events by Type

Filter on canonicalEventType to catch every stored spelling of the type in one call:

curl --get "https://api.suresend.ai/api/partner/events" \
  --data-urlencode "canonicalEventType=Viewed Property" \
  --data-urlencode "limit=100" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

That single request returns events stored as Viewed Property, property_view, viewedProperty,
and Lofty Browse alike. Request several types at once with a comma-separated list:

curl --get "https://api.suresend.ai/api/partner/events" \
  --data-urlencode "canonicalEventType=Viewed Property,Saved Property" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
❗️

Avoid ?eventType=property_view

The eventType filter is an exact, case-sensitive match on the raw value. A team whose events are
stored as Viewed Property returns zero rows for eventType=property_view — with no error to tell
you the match failed. Use canonicalEventType unless you specifically need raw-value matching.

Get Specific Event

Endpoint: GET /api/partner/events/{id}

curl https://api.suresend.ai/api/partner/events/event-uuid \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Updating Events

Endpoint: PUT /api/partner/events/{id}

curl -X PUT https://api.suresend.ai/api/partner/events/event-uuid \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Updated description",
    "metadata": {
      "additionalField": "new value"
    }
  }'

Deleting Events

Endpoint: DELETE /api/partner/events/{id}

curl -X DELETE https://api.suresend.ai/api/partner/events/event-uuid \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Common Use Cases

1. Track Website Activity

// Website tracking script
async function trackPageView(email, pageUrl, pageTitle) {
  await fetch('https://api.suresend.ai/api/partner/events', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      type: 'page_view',
      person: {
        emails: [{ value: email }]
      },
      pageUrl,
      pageTitle,
      pageDuration: Math.round((Date.now() - pageLoadTime) / 1000),
      pageReferrer: document.referrer,
      userAgent: navigator.userAgent
    })
  });
}

2. Track Form Submissions

// Form handler
async function handleFormSubmit(formData) {
  const response = await axios.post(
    'https://api.suresend.ai/api/partner/events',
    {
      type: 'form_submission',
      person: {
        firstName: formData.firstName,
        lastName: formData.lastName,
        emails: [{ value: formData.email, isPrimary: true }],
        phones: [{ value: formData.phone, type: 'mobile', isPrimary: true }]
      },
      message: 'Contact form submitted',
      metadata: {
        formName: 'Contact Us',
        interests: formData.interests,
        message: formData.message
      },
      source: 'website',
      pageUrl: window.location.href
    },
    {
      headers: { Authorization: `Bearer ${API_TOKEN}` }
    }
  );

  const { person, wasPersonCreated } = response.data;
  console.log(wasPersonCreated ? 'New lead created' : 'Existing contact updated');
}

3. Build Engagement Timeline

async function getContactTimeline(personId) {
  const response = await axios.get(
    `https://api.suresend.ai/api/partner/events`,
    {
      params: { personId, limit: 100 },
      headers: { Authorization: `Bearer ${API_TOKEN}` }
    }
  );

  // Group by canonical type — grouping on the raw `eventType` would split the
  // same concept across several buckets ("Viewed Page" vs "page_view").
  const timeline = response.data.events.reduce((acc, event) => {
    const type = event.canonicalEventType;
    if (!acc[type]) acc[type] = [];
    acc[type].push(event);
    return acc;
  }, {});

  return timeline;
}

// Usage
const timeline = await getContactTimeline('person-uuid');
console.log(`Page views: ${timeline['Viewed Page']?.length || 0}`);
console.log(`Property views: ${timeline['Viewed Property']?.length || 0}`);
console.log(`Searches: ${timeline['Property Search']?.length || 0}`);

4. Track Property Interest

async function trackPropertyView(personId, property) {
  await axios.post(
    'https://api.suresend.ai/api/partner/events',
    {
      type: 'property_view',
      personId,
      message: `Viewed ${property.beds}BR home at ${property.address}`,
      property: {
        id: property.id,
        address: property.address,
        price: property.price,
        beds: property.beds,
        baths: property.baths,
        sqft: property.sqft,
        listingUrl: property.url
      },
      pageUrl: window.location.href,
      source: 'website'
    },
    {
      headers: { Authorization: `Bearer ${API_TOKEN}` }
    }
  );

  // Auto-tag based on price range
  if (property.price > 1000000) {
    await applyTag(personId, 'luxury-buyer');
  }
}

// Track when properties are saved to favorites
async function trackSavedProperty(personId, property, savedFrom) {
  await axios.post(
    'https://api.suresend.ai/api/partner/events',
    {
      type: 'property_save',
      personId,
      message: `Saved property to favorites`,
      description: `Saved ${property.beds}BR home at ${property.address}`,
      property: {
        id: property.id,
        address: property.address,
        price: property.price,
        beds: property.beds,
        baths: property.baths,
        sqft: property.sqft,
        propertyType: property.type,
        listingUrl: property.url,
        savedToList: 'Favorites'
      },
      metadata: {
        savedFrom: savedFrom, // 'search results', 'property detail page', etc.
        deviceType: /mobile/i.test(navigator.userAgent) ? 'mobile' : 'desktop',
        interestLevel: 'very-high',
        action: 'saved_to_favorites'
      },
      source: 'website'
    },
    {
      headers: { Authorization: `Bearer ${API_TOKEN}` }
    }
  );

  // Auto-tag high-intent leads
  await applyTag(personId, 'high-intent-buyer');
}

5. Analyze Search Behavior

async function analyzeSearchBehavior(personId) {
  const response = await axios.get(
    `https://api.suresend.ai/api/partner/events`,
    {
      params: { personId, canonicalEventType: 'Property Search', limit: 100 },
      headers: { Authorization: `Bearer ${API_TOKEN}` }
    }
  );

  const searches = response.data.events;

  // Analyze price range preferences
  const priceRanges = searches.map(s => ({
    min: s.propertySearch?.minPrice || 0,
    max: s.propertySearch?.maxPrice || Infinity
  }));

  const avgMinPrice = priceRanges.reduce((sum, r) => sum + r.min, 0) / priceRanges.length;
  const avgMaxPrice = priceRanges.reduce((sum, r) => sum + r.max, 0) / priceRanges.length;

  // Analyze location preferences
  const locations = searches
    .map(s => s.propertySearch?.location)
    .filter(Boolean);

  const topLocations = [...new Set(locations)]
    .map(loc => ({
      location: loc,
      count: locations.filter(l => l === loc).length
    }))
    .sort((a, b) => b.count - a.count);

  return {
    averagePriceRange: { min: avgMinPrice, max: avgMaxPrice },
    topLocations,
    totalSearches: searches.length
  };
}

6. Custom Metadata Tracking

Track rich form data and user preferences:

async function trackContactFormSubmission(formData) {
  await axios.post(
    'https://api.suresend.ai/api/partner/events',
    {
      type: 'form_submission',
      person: {
        firstName: formData.firstName,
        lastName: formData.lastName,
        emails: [{ value: formData.email, isPrimary: true }],
        phones: [{ value: formData.phone, isPrimary: true }]
      },
      message: `${formData.formName} submission`,
      description: formData.message,
      metadata: {
        // Form identification
        formName: formData.formName,
        formId: formData.formId,
        formVersion: '2.0',

        // User preferences
        interests: formData.interests, // ['buying', '3-bedroom', 'garage']
        budget: formData.budget, // '300k-500k'
        timeframe: formData.timeframe, // 'within-3-months'

        // Additional context
        pageUrl: window.location.href,
        referringSource: document.referrer,
        sessionId: getSessionId(),
        deviceType: isMobile() ? 'mobile' : 'desktop',

        // Custom business data
        marketingConsent: formData.marketingConsent,
        preferredContactMethod: formData.preferredContactMethod,
        bestTimeToCall: formData.bestTimeToCall
      },
      source: 'website',
      occurredAt: new Date().toISOString()
    },
    {
      headers: { Authorization: `Bearer ${API_TOKEN}` }
    }
  );
}

Benefits:

  • All form data is preserved in the event
  • Displays nicely in the CRM activity timeline
  • Can be used for filtering and reporting
  • No schema limitations

7. Email Campaign Tracking

// Track email opens
async function trackEmailOpen(personId, campaignId) {
  await axios.post(
    'https://api.suresend.ai/api/partner/events',
    {
      type: 'email_opened',
      personId,
      message: 'Opened marketing email',
      campaign: {
        id: campaignId,
        name: 'Spring 2025 Campaign',
        source: 'mailchimp',
        medium: 'email'
      },
      source: 'email_campaign'
    },
    {
      headers: { Authorization: `Bearer ${API_TOKEN}` }
    }
  );
}

// Track link clicks
async function trackEmailClick(personId, campaignId, linkUrl) {
  await axios.post(
    'https://api.suresend.ai/api/partner/events',
    {
      type: 'email_clicked',
      personId,
      message: `Clicked link: ${linkUrl}`,
      campaign: {
        id: campaignId,
        name: 'Spring 2025 Campaign'
      },
      pageUrl: linkUrl,
      source: 'email_campaign'
    },
    {
      headers: { Authorization: `Bearer ${API_TOKEN}` }
    }
  );
}

8. B2B Lead Tracking with Company

Track form submissions from business contacts with company information:

async function trackB2BFormSubmission(formData) {
  const response = await axios.post(
    'https://api.suresend.ai/api/partner/events',
    {
      type: 'form_submission',
      person: {
        firstName: formData.firstName,
        lastName: formData.lastName,
        emails: [{ value: formData.email, isPrimary: true }],
        phones: [{ value: formData.phone, isPrimary: true }]
      },
      company: {
        name: formData.companyName,
        website: formData.companyWebsite,
        industry: formData.industry,
        companySize: formData.companySize
      },
      message: `B2B inquiry from ${formData.companyName}`,
      metadata: {
        formName: 'B2B Contact Form',
        productInterest: formData.productInterest,
        estimatedDealSize: formData.budget
      },
      source: 'website'
    },
    {
      headers: { Authorization: `Bearer ${API_TOKEN}` }
    }
  );

  const { person, company, wasPersonCreated, wasCompanyCreated } = response.data;

  if (wasCompanyCreated) {
    console.log(`New company created: ${company.name}`);
  } else {
    console.log(`Linked to existing company: ${company.name}`);
  }

  if (wasPersonCreated) {
    console.log(`New contact created: ${person.firstName} ${person.lastName}`);
  }
}

Rate Limiting

The Events API has specific rate limits to handle high-volume tracking:

Rate Limit Headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 85
X-RateLimit-Reset: 3600
X-RateLimit-Context: token:POST.events

Best Practices:

  • Batch events when possible
  • Monitor rate limit headers
  • Implement exponential backoff on 429 errors
  • Contact support for higher limits if needed

Best Practices

Event Naming

  1. Use standard types when one fits - Sending a type from the Common Event Types
    table means it canonicalizes to a known value, so every consumer of your data can match it
  2. Descriptive custom types - Use clear names like property_inquiry not pi
  3. Pick one format and stay with it - The format you send is stored verbatim, so mixing
    page_view and Viewed Page in your own writes leaves both spellings in the raw eventType
    field forever. Any consistent format works; page_view style is the common choice

Reading Event Types

  1. Match on canonicalEventType - Never branch on the raw eventType, which varies by producer
  2. Filter with ?canonicalEventType= - The eventType filter is exact and case-sensitive, and
    returns zero rows rather than an error when the spelling doesn't match
  3. Handle unknown values - The canonical vocabulary can grow, and custom types pass through
    as-is. Prefer a default branch over an exhaustive switch

Data Quality

  1. Always include occurredAt - Track when events actually happened
  2. Provide meaningful messages - Help humans understand the event
  3. Use structured data - Leverage property, propertySearch, campaign fields
  4. Include source/system - Track where events originate

Performance

  1. Async tracking - Don't block user experience waiting for events
  2. Batch when possible - Reduce API calls
  3. Cache person IDs - Avoid repeated person lookups
  4. Handle errors gracefully - Don't fail user actions if tracking fails

Privacy

  1. Respect user consent - Only track if user consents
  2. Anonymize when needed - Don't track PII unnecessarily
  3. Follow regulations - GDPR, CCPA compliance
  4. Provide opt-out - Allow users to stop tracking

Integration Examples

Google Analytics Integration

// Track GA events in CRM
function setupGAIntegration() {
  gtag('event', 'page_view', {
    event_callback: async function(url) {
      if (window.userEmail) {
        await trackPageView(window.userEmail, url, document.title);
      }
    }
  });
}

Segment Integration

// Forward Segment events to CRM
analytics.ready(() => {
  analytics.on('track', async (event, properties) => {
    const user = analytics.user();

    await axios.post(
      'https://api.suresend.ai/api/partner/events',
      {
        type: event.toLowerCase().replace(/ /g, '_'),
        person: {
          emails: [{ value: user.email() }]
        },
        message: event,
        metadata: properties,
        source: 'segment',
        system: 'segment'
      },
      {
        headers: { Authorization: `Bearer ${API_TOKEN}` }
      }
    );
  });
});

Next Steps

Need Help?


Did this page help you?