Integration Partner Widgets
Integration Partner Widgets Guide
Integration Partner Widgets allow third-party developers to embed interactive widgets directly into Sure Send CRM's contact detail pages. These widgets receive secure, signed context about the current contact and agent viewing the page, enabling rich, personalized experiences.
Overview
Partner widgets appear as tabs in contact detail views (both person and company views), allowing agents to access your integration's features without leaving the CRM. Examples include:
- Property recommendation engines
- Market analysis tools
- Document preparation services
- Communication tools
- Task automation interfaces
- Analytics dashboards
- Company research tools
- B2B lead enrichment services
How It Works
sequenceDiagram
participant Agent
participant CRM
participant Widget
Agent->>CRM: Opens contact detail page
CRM->>CRM: Generates signed context
CRM->>Widget: Loads iframe with context + signature
Widget->>Widget: Verifies signature
Widget->>Agent: Displays personalized content
Getting Started
1. Prerequisites
Before creating a widget, you'll need:
- A web application that can be embedded in an iframe
- Ability to verify HMAC SHA-256 signatures
- HTTPS endpoint for your widget
- Understanding of iframe security and postMessage communication
2. Widget Registration
Contact the Sure Send CRM team to register your widget. You'll need to provide:
- Widget Name: Display name shown in the tab (e.g., "Property Search")
- Description: Rich text description of your widget's functionality
- Widget URL: HTTPS endpoint where your widget is hosted
- View Types: Specify which views your widget supports:
- Person View: Widget appears in person detail pages
- Company View: Widget appears in company detail pages
- Both: Widget appears in both person and company detail pages
- Team ID (optional): For testing before global approval
You'll receive:
- Secret Key: A secure key for signature verification (64 characters, hex-encoded)
- Widget ID: Unique identifier for your widget
Note: You can specify one or both view types. At least one view type must be selected. Teams can enable your widget separately for person and company views based on their needs.
3. Approval Process
Widgets go through a two-stage approval process:
- Team Testing: When created with a
team_id, the widget is visible only to that team for testing - Global Approval: After testing, request approval from Sure Send CRM to make the widget available to all users
Widget Implementation
URL Structure
Your widget will be loaded in an iframe with the following query parameters:
https://your-widget-url.com/widget?context=BASE64_ENCODED_DATA&signature=HMAC_SIGNATURE
Parameters:
context: Base64-encoded JSON containing contact, agent, and team informationsignature: HMAC SHA-256 signature of the context using your secret key
Context Data Structure
After decoding the context parameter, you'll receive a JSON object. The structure depends on whether the widget is being viewed from a person detail page or a company detail page.
Person View Context
When viewing from a person detail page, the context contains a person object:
{
"team": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Acme Real Estate"
},
"user": {
"id": "123e4567-e89b-12d3-a456-426614174001",
"email": "[email protected]"
},
"person": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"firstName": "Jane",
"lastName": "Doe",
"stage": {
"name": "Lead"
},
"emails": [
{
"id": "email-uuid",
"email": "[email protected]",
"value": "[email protected]",
"isPrimary": 1,
"created_at": "2025-10-01T12:00:00Z",
"updated_at": "2025-10-01T12:00:00Z"
}
],
"phones": [
{
"id": "phone-uuid",
"phone": "+1-555-1234",
"type": "mobile",
"isPrimary": 1,
"normalized": "15551234",
"created_at": "2025-10-01T12:00:00Z",
"updated_at": "2025-10-01T12:00:00Z"
}
]
},
"timestamp": "2025-10-27T15:30:00Z"
}Company View Context
When viewing from a company detail page, the context contains a company object instead:
{
"team": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Acme Real Estate"
},
"user": {
"id": "123e4567-e89b-12d3-a456-426614174001",
"email": "[email protected]"
},
"company": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Acme Corporation",
"industry": "Real Estate",
"website": "https://acme.com",
"description": "Leading real estate development company",
"companySize": "201-500",
"annualRevenue": 50000000,
"foundedDate": "2010-01-15T00:00:00Z",
"registrationNumber": "REG-12345",
"taxId": "TAX-67890",
"stage": {
"name": "Customer"
},
"emails": [
{
"id": "email-uuid",
"email": "[email protected]",
"value": "[email protected]",
"isPrimary": 1,
"created_at": "2025-10-01T12:00:00Z",
"updated_at": "2025-10-01T12:00:00Z"
}
],
"phones": [
{
"id": "phone-uuid",
"phone": "+1-555-1234",
"type": "work",
"isPrimary": 1,
"normalized": "15551234",
"created_at": "2025-10-01T12:00:00Z",
"updated_at": "2025-10-01T12:00:00Z"
}
],
"addresses": [
{
"id": "address-uuid",
"type": "office",
"street": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"code": "94105",
"country": "United States",
"isPrimary": 1
}
]
},
"timestamp": "2025-10-27T15:30:00Z"
}Important: Your widget should check for the presence of either person or company in the context to determine which view type it's being loaded in.
Determining View Type
Your widget code should check which type of context it received:
// JavaScript example
const data = parseContext(context);
if (data.person) {
// This is a person view context
displayPersonWidget(data.person);
} else if (data.company) {
// This is a company view context
displayCompanyWidget(data.company);
} else {
// Invalid context - should not happen
showError('Invalid context: missing person or company data');
}# Python example
data = parse_context(context)
if 'person' in data:
# This is a person view context
render_person_widget(data['person'])
elif 'company' in data:
# This is a company view context
render_company_widget(data['company'])
else:
# Invalid context
return 'Invalid context', 400Common Fields (present in all contexts):
| Field | Type | Description |
|---|---|---|
team.id | string | UUID of the agent's current team |
team.name | string | Name of the agent's current team |
user.id | string | UUID of the agent viewing the page |
user.email | string | Email of the agent viewing the page |
timestamp | string | ISO 8601 timestamp when the context was generated |
Person Fields (only present when viewing from person detail page):
| Field | Type | Description |
|---|---|---|
person.id | string | UUID of the person contact |
person.firstName | string | Person's first name |
person.lastName | string | Person's last name |
person.stage.name | string | Current stage of the person (e.g., "Lead", "Prospect") |
person.emails | array | All email addresses for the person |
person.phones | array | All phone numbers for the person |
Company Fields (only present when viewing from company detail page):
| Field | Type | Description |
|---|---|---|
company.id | string | UUID of the company |
company.name | string | Company name |
company.industry | string | Industry/sector (e.g., "Real Estate", "Technology") |
company.website | string | Company website URL |
company.description | string | Company description |
company.companySize | string | Company size range (e.g., "1-10", "201-500", "5001+") |
company.annualRevenue | number | Annual revenue (decimal or integer) |
company.foundedDate | string | ISO 8601 date when company was founded |
company.registrationNumber | string | Business registration number |
company.taxId | string | Tax ID number |
company.stage.name | string | Current stage of the company (e.g., "Lead", "Customer") |
company.emails | array | All email addresses for the company |
company.phones | array | All phone numbers for the company |
company.addresses | array | All addresses for the company |
Email Object:
| Field | Type | Description |
|---|---|---|
id | string | UUID of the email record |
email | string | Email address (original case) |
value | string | Email address (lowercase) |
isPrimary | integer | 1 if primary, 0 otherwise |
created_at | string | ISO 8601 timestamp |
updated_at | string | ISO 8601 timestamp |
Phone Object:
| Field | Type | Description |
|---|---|---|
id | string | UUID of the phone record |
phone | string | Phone number (formatted) |
type | string | Type: "mobile", "home", "work", etc. |
isPrimary | integer | 1 if primary, 0 otherwise |
normalized | string | Phone number with only digits |
created_at | string | ISO 8601 timestamp |
updated_at | string | ISO 8601 timestamp |
Address Object (Company contexts only):
| Field | Type | Description |
|---|---|---|
id | string | UUID of the address record |
type | string | Address type: "office", "billing", "shipping", etc. |
street | string | Street address |
city | string | City name |
state | string | State or province |
code | string | Postal/ZIP code |
country | string | Country name |
isPrimary | integer | 1 if primary, 0 otherwise |
Signature Verification
CRITICAL: Always verify the signature before trusting the context data. This prevents tampering and ensures the data comes from Sure Send CRM.
Verification Steps
- Extract the
contextandsignaturequery parameters - Compute HMAC SHA-256 of the
contextstring (Base64-encoded, not decoded) using your secret key - Compare the computed signature with the provided signature using constant-time comparison
- Only decode and use the context if signatures match
Example Implementations
Node.js:
const crypto = require('crypto');
function verifySignature(context, signature, secretKey) {
// Compute HMAC SHA-256 of the Base64-encoded context
const computedSignature = crypto
.createHmac('sha256', secretKey)
.update(context)
.digest('hex');
// Use constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(computedSignature)
);
}
function parseContext(contextBase64) {
const contextJson = Buffer.from(contextBase64, 'base64').toString('utf-8');
return JSON.parse(contextJson);
}
// Usage in your widget
const urlParams = new URLSearchParams(window.location.search);
const context = urlParams.get('context');
const signature = urlParams.get('signature');
if (verifySignature(context, signature, YOUR_SECRET_KEY)) {
const data = parseContext(context);
// Check if this is a person or company context
if (data.person) {
console.log('Person:', data.person.firstName, data.person.lastName);
} else if (data.company) {
console.log('Company:', data.company.name);
}
} else {
console.error('Invalid signature!');
}Python (Flask):
import hmac
import hashlib
import base64
import json
from flask import request
def verify_signature(context, signature, secret_key):
"""Verify HMAC SHA-256 signature"""
computed_signature = hmac.new(
secret_key.encode('utf-8'),
context.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Use constant-time comparison
return hmac.compare_digest(signature, computed_signature)
def parse_context(context_base64):
"""Decode and parse the context"""
context_json = base64.b64decode(context_base64).decode('utf-8')
return json.loads(context_json)
@app.route('/widget')
def widget():
context = request.args.get('context')
signature = request.args.get('signature')
if not verify_signature(context, signature, YOUR_SECRET_KEY):
return 'Invalid signature', 403
data = parse_context(context)
# Check if this is a person or company context
if 'person' in data:
contact = data['person']
contact_type = 'person'
elif 'company' in data:
contact = data['company']
contact_type = 'company'
else:
return 'Invalid context', 400
return render_template('widget.html', contact=contact, contact_type=contact_type)Ruby:
require 'openssl'
require 'base64'
require 'json'
def verify_signature(context, signature, secret_key)
computed_signature = OpenSSL::HMAC.hexdigest(
'sha256',
secret_key,
context
)
# Use constant-time comparison
Rack::Utils.secure_compare(signature, computed_signature)
end
def parse_context(context_base64)
context_json = Base64.strict_decode64(context_base64)
JSON.parse(context_json)
end
# Usage in Sinatra/Rails controller
context = params[:context]
signature = params[:signature]
if verify_signature(context, signature, YOUR_SECRET_KEY)
data = parse_context(context)
# Check if this is a person or company context
if data['person']
@contact = data['person']
@contact_type = 'person'
elsif data['company']
@contact = data['company']
@contact_type = 'company'
else
halt 400, 'Invalid context'
end
render :widget
else
halt 403, 'Invalid signature'
endPHP:
<?php
function verifySignature($context, $signature, $secretKey) {
$computedSignature = hash_hmac('sha256', $context, $secretKey);
return hash_equals($signature, $computedSignature);
}
function parseContext($contextBase64) {
$contextJson = base64_decode($contextBase64);
return json_decode($contextJson, true);
}
// Usage
$context = $_GET['context'];
$signature = $_GET['signature'];
if (verifySignature($context, $signature, YOUR_SECRET_KEY)) {
$data = parseContext($context);
// Check if this is a person or company context
if (isset($data['person'])) {
$contact = $data['person'];
echo "Person: " . $contact['firstName'] . " " . $contact['lastName'];
} elseif (isset($data['company'])) {
$contact = $data['company'];
echo "Company: " . $contact['name'];
}
} else {
http_response_code(403);
echo "Invalid signature";
}
?>Security Best Practices
1. Always Verify Signatures
Never trust context data without verifying the signature. Malicious users could craft fake contexts.
// ❌ BAD: Using context without verification
const data = parseContext(context);
// ✅ GOOD: Verify first, then use
if (verifySignature(context, signature, secretKey)) {
const data = parseContext(context);
}2. Keep Secret Keys Secure
- Never commit secret keys to version control
- Store keys in environment variables or secure vaults
- Use different keys for development and production
- Rotate keys periodically
3. Use Constant-Time Comparison
Prevent timing attacks by using constant-time comparison functions:
// ❌ BAD: Vulnerable to timing attacks
if (signature === computedSignature) { ... }
// ✅ GOOD: Constant-time comparison
if (crypto.timingSafeEqual(signature, computedSignature)) { ... }4. Validate Context Timestamp
Check that the context is recent to prevent replay attacks:
function isContextFresh(context, maxAgeSeconds = 300) {
const timestamp = new Date(context.timestamp);
const now = new Date();
const ageSeconds = (now - timestamp) / 1000;
return ageSeconds <= maxAgeSeconds;
}5. Iframe Security
Set appropriate headers on your widget endpoint:
X-Frame-Options: ALLOW-FROM https://app.suresend.ai
Content-Security-Policy: frame-ancestors https://app.suresend.ai
6. HTTPS Only
Your widget URL must use HTTPS. HTTP URLs will be rejected.
Widget Design Guidelines
User Experience
- Fast Loading: Optimize for quick load times. Users expect instant feedback.
- Responsive Design: Support various widget sizes and screen resolutions
- Loading States: Show loading indicators while fetching data
- Error Handling: Display user-friendly error messages
- Accessibility: Follow WCAG 2.1 guidelines
Visual Design
- Consistent Branding: Use your brand colors but ensure readability
- Sure Send CRM Compatibility: Design should feel native to the CRM
- Mobile Friendly: Support responsive layouts
- Dark Mode (optional): Consider supporting dark mode
Example Widget HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Property Recommendations</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: white;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.error {
background: #fee;
border: 1px solid #fcc;
padding: 15px;
border-radius: 4px;
color: #c33;
}
.contact-header {
background: #f5f5f5;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.property-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
transition: box-shadow 0.2s;
}
.property-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div id="app">
<div class="loading">Loading...</div>
</div>
<script>
const SECRET_KEY = 'YOUR_SECRET_KEY_HERE';
async function initWidget() {
const urlParams = new URLSearchParams(window.location.search);
const context = urlParams.get('context');
const signature = urlParams.get('signature');
if (!context || !signature) {
showError('Missing context or signature');
return;
}
if (!await verifySignature(context, signature)) {
showError('Invalid signature');
return;
}
const data = parseContext(context);
renderWidget(data);
}
async function verifySignature(context, signature) {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(SECRET_KEY),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signatureBuffer = await crypto.subtle.sign(
'HMAC',
key,
encoder.encode(context)
);
const computed = Array.from(new Uint8Array(signatureBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return signature === computed;
}
function parseContext(contextBase64) {
const contextJson = atob(contextBase64);
return JSON.parse(contextJson);
}
function renderWidget(data) {
const app = document.getElementById('app');
let contact, contactType, contactName;
// Determine if this is person or company context
if (data.person) {
contact = data.person;
contactType = 'person';
contactName = `${contact.firstName} ${contact.lastName}`;
} else if (data.company) {
contact = data.company;
contactType = 'company';
contactName = contact.name;
} else {
showError('Invalid context: missing person or company data');
return;
}
app.innerHTML = `
<div class="contact-header">
<h2>${contactName}</h2>
<p>Type: ${contactType === 'person' ? 'Person' : 'Company'}</p>
<p>Stage: ${contact.stage?.name || 'Unknown'}</p>
<p>Agent: ${data.user.email}</p>
${contactType === 'company' ? `<p>Industry: ${contact.industry || 'N/A'}</p>` : ''}
</div>
<h3>Recommended Properties</h3>
<div class="property-card">
<h4>Beautiful 3BR Home</h4>
<p>123 Main St, Anytown, USA</p>
<p><strong>$450,000</strong></p>
<button onclick="viewProperty('prop-123')">View Details</button>
</div>
<div class="property-card">
<h4>Spacious 4BR Colonial</h4>
<p>456 Oak Ave, Anytown, USA</p>
<p><strong>$575,000</strong></p>
<button onclick="viewProperty('prop-456')">View Details</button>
</div>
`;
}
function showError(message) {
const app = document.getElementById('app');
app.innerHTML = `<div class="error">${message}</div>`;
}
function viewProperty(id) {
alert('Viewing property: ' + id);
// In a real widget, you'd navigate or show details
}
// Initialize on page load
initWidget();
</script>
</body>
</html>Testing Your Widget
Local Development
- Create a test URL with sample context and signature:
const crypto = require('crypto');
// Example: Person context
const personContext = {
team: {
id: "team-uuid-here",
name: "Test Team"
},
user: {
id: "user-uuid-here",
email: "[email protected]"
},
person: {
id: "123e4567-e89b-12d3-a456-426614174000",
firstName: "Test",
lastName: "User",
stage: { name: "Lead" },
emails: [{ value: "[email protected]", isPrimary: 1 }],
phones: [{ phone: "+1-555-1234", type: "mobile", isPrimary: 1, normalized: "15551234" }]
},
timestamp: new Date().toISOString()
};
// Example: Company context
const companyContext = {
team: {
id: "team-uuid-here",
name: "Test Team"
},
user: {
id: "user-uuid-here",
email: "[email protected]"
},
company: {
id: "123e4567-e89b-12d3-a456-426614174000",
name: "Test Company",
industry: "Real Estate",
website: "https://test.com",
stage: { name: "Lead" },
emails: [{ value: "[email protected]", isPrimary: 1 }],
phones: [{ phone: "+1-555-1234", type: "work", isPrimary: 1, normalized: "15551234" }],
addresses: [{
type: "office",
street: "123 Main St",
city: "San Francisco",
state: "CA",
code: "94105",
country: "United States",
isPrimary: 1
}]
},
timestamp: new Date().toISOString()
};
function generateTestUrl(context) {
const contextBase64 = Buffer.from(JSON.stringify(context)).toString('base64');
const signature = crypto.createHmac('sha256', YOUR_SECRET_KEY)
.update(contextBase64)
.digest('hex');
return `http://localhost:3000/widget?context=${contextBase64}&signature=${signature}`;
}
console.log('Person view:', generateTestUrl(personContext));
console.log('Company view:', generateTestUrl(companyContext));- Open the URL in your browser to test signature verification and rendering
Team Testing
- Request a test widget with your team's
team_idand specify which view types your widget supports - Enable the widget from Settings > Integration Partner Widgets for the desired view types:
- Enable for Person View: Widget appears in person detail pages
- Enable for Company View: Widget appears in company detail pages
- Navigate to person or company detail pages (depending on your enabled view types)
- Your widget will appear as a tab in the enabled views
Pre-Launch Checklist
- Signature verification is working correctly
- Context timestamp validation is implemented
- Widget correctly handles both person and company contexts (if supporting both view types)
- Error handling covers all edge cases
- Loading states are shown during async operations
- Widget loads in under 2 seconds
- Mobile responsive design works
- No console errors or warnings
- HTTPS endpoint is configured
- Security headers are set
- Secret key is stored securely (not in code)
User Enablement
Once your widget is approved and published:
- Users discover your widget in Settings > Integration Partner Widgets
- Users enable the widget by toggling it on for the desired view types:
- Enable for Person View: Widget appears in person detail pages
- Enable for Company View: Widget appears in company detail pages
- Enable for Both: Widget appears in both view types
- Widget appears as a tab on the enabled view types for all team members
- Users can disable at any time from the same settings page, independently for each view type
Note: Teams can enable your widget separately for person and company views. If your widget supports both view types, teams can choose to enable it in one or both views based on their needs.
API Reference
Widget URL Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
context | string | Yes | Base64-encoded JSON context |
signature | string | Yes | HMAC SHA-256 signature of context |
Context Schema
See Context Data Structure section above for the complete schema.
Troubleshooting
Common Issues
Problem: "Invalid signature" error
Solutions:
- Ensure you're using the correct secret key
- Verify you're signing the Base64-encoded context (not the decoded JSON)
- Check for leading/trailing whitespace in the secret key
- Use constant-time comparison functions
Problem: Widget not loading
Solutions:
- Check that your URL is HTTPS
- Verify X-Frame-Options and CSP headers allow framing
- Check browser console for JavaScript errors
- Test signature verification endpoint separately
Problem: Context seems old or stale
Solutions:
- Implement timestamp validation
- Check if user's system clock is correct
- Verify timezone handling in your code
Problem: Widget loads slowly
Solutions:
- Optimize initial bundle size
- Lazy load non-critical features
- Use CDN for static assets
- Implement proper caching headers
Changelog
-
2025-10-30: Added company view support
- Widgets can now be configured for person view, company view, or both
- Company context includes business-specific fields (industry, company size, revenue, addresses)
- Teams can enable widgets separately for person and company views
- Updated context structure to support both person and company data
-
2025-10-27: Initial release
- Widget registration and approval process
- Secure context passing with HMAC SHA-256 signatures
- Team-based testing before global approval
- Person view support
Updated 9 days ago
