Skip to main content

Constituent Management Integration Guide

A comprehensive guide to managing constituents (people) in Oho, including creation, bulk operations, HR system integration, and lifecycle management.

Overview

Base URL: https://app.weareoho.com/api

Authentication: Bearer token (JWT) required in Authorization header

Content-Type: application/json

This guide covers everything you need to effectively manage constituents in Oho, from individual creation to bulk operations and HR system integration.

Why This Guide?

Constituents are the foundation of your compliance program. Effective constituent management enables:

  • Centralized person records - Single source of truth for employee/volunteer data
  • Streamlined onboarding - Quick setup for new hires with compliance tracking
  • HR system integration - Sync with existing workforce management systems
  • Lifecycle tracking - Manage employees from hire to offboarding
  • Bulk operations - Import and update hundreds of records efficiently
  • Audit trails - Complete history of checks linked to each person

Prerequisites

Before You Begin

Ensure you have:


Core Concepts

What is a Constituent?

A constituent represents an individual person in your organization who requires background checks or compliance screening. Think of constituents as your employee/volunteer records in Oho.

Key characteristics:

  • Stores personal information (name, DOB, email, phone)
  • Links to multiple accreditations (background checks) over time
  • Belongs to exactly one organization
  • Can be active or inactive
  • Optionally links to your HR system via external_id

Constituent vs Accreditation

Understanding the relationship:

Constituent (Person Record)
├── Personal Details (name, email, DOB)
├── External ID (link to your HR system)
└── Accreditations (Background Checks)
├── VIC WWC (2024-01-15) - Active
├── NDIS Screening (2024-03-10) - Cleared
└── QLD Blue Card (2025-01-05) - Active

Constituents = WHO is being checked Accreditations = WHAT checks have been performed


Creating Constituents

Method 1: Create Individual Constituent

The primary endpoint for creating constituents is POST /constituents/ensure_exists. This endpoint is idempotent and will either create a new constituent or return an existing one based on email.

Basic Creation

curl -X POST https://app.weareoho.com/api/constituents/ensure_exists \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"first_name": "Sarah",
"last_name": "Chen",
"email": "sarah.chen@example.com"
}'

Response:

{
"id": 12345,
"first_name": "Sarah",
"last_name": "Chen",
"email": "sarah.chen@example.com",
"phone": null,
"mobile_number": null,
"date_of_birth": null,
"external_id": null,
"active": true,
"created_at": "2024-11-05T10:00:00Z",
"updated_at": "2024-11-05T10:00:00Z"
}

Full Details Creation

curl -X POST https://app.weareoho.com/api/constituents/ensure_exists \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"first_name": "Michael",
"last_name": "Thompson",
"email": "michael.thompson@example.com",
"phone": "+61-2-9876-5432",
"mobile_number": "+61-412-345-678",
"date_of_birth": "1988-07-22",
"external_id": "EMP-2024-1234"
}'

Response includes all provided fields plus:

  • id - Oho's unique identifier for this constituent
  • active - Defaults to true
  • created_at and updated_at timestamps
External ID Best Practice

Always provide external_id to link Oho constituents to your HR system. This makes syncing and cross-referencing much easier.

Method 2: Check if Constituent Exists

Before creating, you can check if a constituent already exists:

curl -X POST https://app.weareoho.com/api/constituents/check_exists \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "sarah.chen@example.com"
}'

Response (if exists):

{
"exists": true,
"constituent_id": 12345
}

Response (if not exists):

{
"exists": false,
"constituent_id": null
}
When to Use check_exists vs ensure_exists
  • Use check_exists when you need to know if a constituent exists before taking action
  • Use ensure_exists when you want to create if missing or get existing (idempotent)
  • ensure_exists is typically preferred for most integrations

Lifecycle Management

Onboarding Workflow

Complete workflow for new employee onboarding:

New Employee Hired

1. Create Constituent
POST /constituents/ensure_exists

2. Submit Required Checks
POST /api/scan (WWC, NDIS, etc.)

3. Monitor via Webhooks
Receive completion notifications

4. Verify Compliance
GET /constituents/{id}

Employee Cleared to Start

Complete Example:

import requests
import time

JWT_TOKEN = "your-jwt-token"
BASE_URL = "https://app.weareoho.com/api"

headers = {
"Authorization": f"Bearer {JWT_TOKEN}",
"Content-Type": "application/json"
}

def onboard_employee(employee_data):
"""Complete onboarding workflow"""

print(f"Onboarding {employee_data['first_name']} {employee_data['last_name']}")

# Step 1: Create constituent
print("Step 1: Creating constituent...")
constituent_response = requests.post(
f"{BASE_URL}/constituents/ensure_exists",
headers=headers,
json={
"first_name": employee_data["first_name"],
"last_name": employee_data["last_name"],
"email": employee_data["email"],
"phone": employee_data.get("phone"),
"date_of_birth": employee_data.get("date_of_birth"),
"external_id": employee_data.get("employee_id")
}
)

constituent = constituent_response.json()
print(f"✓ Constituent created: ID {constituent['id']}")

# Step 2: Submit required background checks
print("\nStep 2: Submitting background checks...")

checks_to_submit = []

# Determine which checks are required based on role/location
if employee_data.get("works_with_children"):
if employee_data["state"] == "VIC":
checks_to_submit.append({
"type": "vicwwc",
"identifier": employee_data["wwc_number"],
"name": "Victorian WWC"
})
elif employee_data["state"] == "NSW":
checks_to_submit.append({
"type": "nswwwc",
"identifier": employee_data["wwc_number"],
"name": "NSW WWC"
})

# Submit all checks
correlation_ids = []
for check in checks_to_submit:
check_response = requests.post(
f"{BASE_URL}/api/scan",
headers=headers,
json={
"type": check["type"],
"identifier": check["identifier"],
"first_name": employee_data["first_name"],
"surname": employee_data["last_name"],
"birth_date": employee_data.get("date_of_birth"),
"constituent": {"id": constituent["id"]}
}
)

if check_response.status_code == 200:
correlation_id = check_response.json()["correlation_id"]
correlation_ids.append(correlation_id)
print(f"✓ Submitted {check['name']}: {correlation_id}")
else:
print(f"✗ Failed to submit {check['name']}: {check_response.text}")

print(f"\n✓ Onboarding initiated for {employee_data['first_name']} {employee_data['last_name']}")
print(f" Constituent ID: {constituent['id']}")
print(f" Checks submitted: {len(correlation_ids)}")
print(f" Correlation IDs: {correlation_ids}")

return {
"constituent_id": constituent["id"],
"correlation_ids": correlation_ids
}

# Example usage
new_employee = {
"employee_id": "EMP-2024-5678",
"first_name": "Jessica",
"last_name": "Martinez",
"email": "jessica.martinez@example.com",
"phone": "+61-3-9876-5432",
"date_of_birth": "1992-05-14",
"state": "VIC",
"works_with_children": True,
"wwc_number": "1234567A"
}

result = onboard_employee(new_employee)

Offboarding Workflow

def offboard_employee(external_id):
"""Offboard an employee (mark as inactive)"""

print(f"Offboarding employee: {external_id}")

# Step 1: Find constituent by external_id
list_response = requests.get(
f"{BASE_URL}/constituents",
headers=headers
)

constituents = list_response.json()
constituent = next(
(c for c in constituents if c.get("external_id") == external_id),
None
)

if not constituent:
print(f"✗ Constituent not found with external_id: {external_id}")
return False

# Step 2: Mark as inactive
update_response = requests.patch(
f"{BASE_URL}/constituents/{constituent['id']}",
headers=headers,
json={"active": False}
)

if update_response.status_code == 200:
print(f"✓ Constituent {constituent['id']} marked as inactive")
return True
else:
print(f"✗ Failed to update constituent: {update_response.text}")
return False

# Example usage
offboard_employee("EMP-2024-1234")

Updating Constituents

Update Individual Fields

curl -X PATCH https://app.weareoho.com/api/constituents/12345 \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "newemail@example.com",
"phone": "+61-2-8888-9999",
"active": true
}'

Auto-Enrichment Pattern

When submitting checks with a constituent ID, Oho may auto-update constituent fields with data from the check:

# Constituent initially created with minimal data
constituent_response = requests.post(
f"{BASE_URL}/constituents/ensure_exists",
headers=headers,
json={
"first_name": "Tom",
"last_name": "Wilson",
"email": "tom.wilson@example.com"
# No date_of_birth provided
}
)

constituent_id = constituent_response.json()["id"]

# Submit check with full details
check_response = requests.post(
f"{BASE_URL}/api/scan",
headers=headers,
json={
"type": "vicwwc",
"identifier": "9876543A",
"first_name": "Tom",
"surname": "Wilson",
"birth_date": "1990-08-05", # Full details provided
"constituent": {"id": constituent_id}
}
)

# The constituent record may now include date_of_birth
Auto-Enrichment Warning

Auto-enrichment updates replace existing values and cannot be undone via the UI. Only provide accurate data when submitting checks.


Retrieving Constituents

List All Constituents

curl -X GET https://app.weareoho.com/api/constituents \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

Response:

[
{
"id": 12345,
"first_name": "Sarah",
"last_name": "Chen",
"email": "sarah.chen@example.com",
"phone": "+61-2-9876-5432",
"date_of_birth": "1992-03-15",
"external_id": "EMP-001",
"active": true,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-11-05T14:30:00Z"
},
// ... more constituents
]

Get Individual Constituent

Retrieve a single constituent with all linked accreditations:

curl -X GET https://app.weareoho.com/api/constituents/12345 \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

Response includes accreditations:

{
"id": 12345,
"first_name": "Sarah",
"last_name": "Chen",
"email": "sarah.chen@example.com",
"active": true,
"accreditations": [
{
"id": 5001,
"type": "vicwwc",
"identifier": "1234567A",
"status": "completed",
"registry_response": {
"may_engage": true,
"oho_status": "active",
"expiry_date": "2027-06-15"
},
"completed_at": "2024-01-15T10:30:00Z"
},
{
"id": 5002,
"type": "ndis",
"identifier": "NDIS-123456",
"status": "completed",
"registry_response": {
"clearance": "cleared"
},
"completed_at": "2024-03-10T09:15:00Z"
}
]
}

Filter Active/Inactive

# Get only active constituents
response = requests.get(
f"{BASE_URL}/constituents",
headers=headers
)

constituents = response.json()
active_constituents = [c for c in constituents if c["active"]]
inactive_constituents = [c for c in constituents if not c["active"]]

print(f"Active: {len(active_constituents)}")
print(f"Inactive: {len(inactive_constituents)}")

Handling Duplicates

Email Uniqueness

Emails must be unique within an organization. Attempting to create a duplicate will either:

  • Return the existing constituent (with ensure_exists)
  • Fail with validation error (with direct creation)

Example - Duplicate Detection:

# First creation
response1 = requests.post(
f"{BASE_URL}/constituents/ensure_exists",
headers=headers,
json={
"first_name": "John",
"last_name": "Smith",
"email": "john.smith@example.com"
}
)
constituent1 = response1.json()
print(f"First call - ID: {constituent1['id']}")

# Second creation with same email
response2 = requests.post(
f"{BASE_URL}/constituents/ensure_exists",
headers=headers,
json={
"first_name": "Johnny", # Different first name
"last_name": "Smith",
"email": "john.smith@example.com" # Same email
}
)
constituent2 = response2.json()
print(f"Second call - ID: {constituent2['id']}")

# Same ID returned
assert constituent1["id"] == constituent2["id"]

Using external_id to Prevent Duplicates

def find_or_create_constituent(hr_employee):
"""Find constituent by external_id or create new"""

# Get all constituents
response = requests.get(
f"{BASE_URL}/constituents",
headers=headers
)

constituents = response.json()

# Look for existing by external_id
existing = next(
(c for c in constituents if c.get("external_id") == hr_employee["employee_id"]),
None
)

if existing:
print(f"Found existing constituent: {existing['id']}")
return existing

# Create new if not found
response = requests.post(
f"{BASE_URL}/constituents/ensure_exists",
headers=headers,
json={
"first_name": hr_employee["first_name"],
"last_name": hr_employee["last_name"],
"email": hr_employee["email"],
"external_id": hr_employee["employee_id"]
}
)

new_constituent = response.json()
print(f"Created new constituent: {new_constituent['id']}")
return new_constituent

Common Workflows

Workflow 1: New Hire with Multiple Checks

def onboard_with_multiple_checks(employee):
"""Onboard employee with state-specific checks"""

# Create constituent
constituent = requests.post(
f"{BASE_URL}/constituents/ensure_exists",
headers=headers,
json={
"first_name": employee["first_name"],
"last_name": employee["last_name"],
"email": employee["email"],
"date_of_birth": employee["date_of_birth"],
"external_id": employee["employee_id"]
}
).json()

# Determine required checks
checks = []

if employee["state"] == "VIC":
checks.append(("vicwwc", employee["vic_wwc"]))
elif employee["state"] == "NSW":
checks.append(("nswwwc", employee["nsw_wwc"]))

if employee["requires_ndis"]:
checks.append(("ndis", employee["ndis_number"]))

# Submit all checks
correlation_ids = []
for check_type, identifier in checks:
response = requests.post(
f"{BASE_URL}/api/scan",
headers=headers,
json={
"type": check_type,
"identifier": identifier,
"first_name": employee["first_name"],
"surname": employee["last_name"],
"birth_date": employee["date_of_birth"],
"constituent": {"id": constituent["id"]}
}
)
correlation_ids.append(response.json()["correlation_id"])

return {
"constituent_id": constituent["id"],
"checks_submitted": len(correlation_ids),
"correlation_ids": correlation_ids
}

Workflow 2: Compliance Audit Report

Generate a compliance report for all active employees:

def generate_compliance_report():
"""Generate compliance status for all active constituents"""

# Get all constituents
response = requests.get(
f"{BASE_URL}/constituents",
headers=headers
)

constituents = response.json()
active_constituents = [c for c in constituents if c["active"]]

report = []

for constituent in active_constituents:
# Get full details with accreditations
detail_response = requests.get(
f"{BASE_URL}/constituents/{constituent['id']}",
headers=headers
)

details = detail_response.json()
accreditations = details.get("accreditations", [])

# Check compliance status
has_valid_wwc = any(
acc["type"] in ["vicwwc", "nswwwc", "qldblue", "sawwc", "wawwc", "taswwc", "ntwwc", "actwwc"]
and acc["status"] == "completed"
and acc.get("registry_response", {}).get("oho_status") == "active"
for acc in accreditations
)

report.append({
"name": f"{constituent['first_name']} {constituent['last_name']}",
"email": constituent["email"],
"external_id": constituent.get("external_id"),
"active": constituent["active"],
"total_checks": len(accreditations),
"has_valid_wwc": has_valid_wwc,
"compliant": has_valid_wwc # Add more conditions as needed
})

return report

# Generate and print report
report = generate_compliance_report()

print(f"\nCompliance Report - {len(report)} active employees")
print("=" * 80)

compliant = sum(1 for r in report if r["compliant"])
non_compliant = len(report) - compliant

print(f"Compliant: {compliant}")
print(f"Non-compliant: {non_compliant}")

print("\nNon-compliant employees:")
for employee in report:
if not employee["compliant"]:
print(f" - {employee['name']} ({employee['email']})")

Workflow 3: Bulk Status Check

Check compliance status for specific employees:

def check_bulk_compliance(external_ids):
"""Check compliance for specific employees"""

# Get all constituents
response = requests.get(
f"{BASE_URL}/constituents",
headers=headers
)

constituents = response.json()

results = []

for external_id in external_ids:
constituent = next(
(c for c in constituents if c.get("external_id") == external_id),
None
)

if not constituent:
results.append({
"external_id": external_id,
"found": False,
"compliant": False
})
continue

# Get details with accreditations
detail_response = requests.get(
f"{BASE_URL}/constituents/{constituent['id']}",
headers=headers
)

details = detail_response.json()
accreditations = details.get("accreditations", [])

# Check for active WWC
has_wwc = any(
acc["type"].endswith("wwc")
and acc["status"] == "completed"
and acc.get("registry_response", {}).get("oho_status") == "active"
for acc in accreditations
)

results.append({
"external_id": external_id,
"found": True,
"name": f"{constituent['first_name']} {constituent['last_name']}",
"email": constituent["email"],
"active": constituent["active"],
"checks_count": len(accreditations),
"has_wwc": has_wwc,
"compliant": has_wwc
})

return results

# Check specific employees
employees_to_check = ["EMP-001", "EMP-002", "EMP-003"]
results = check_bulk_compliance(employees_to_check)

for result in results:
status = "✓ Compliant" if result["compliant"] else "✗ Non-compliant"
print(f"{result['external_id']}: {status}")

Error Handling

Common Errors

1. Duplicate Email

Error:

{
"error": {
"code": "duplicate_email",
"message": "Email already exists for another constituent"
}
}

Solution:

  • Use ensure_exists which returns existing constituent
  • Or check existence first with check_exists
  • Update existing constituent instead of creating new

2. Invalid Email Format

Error:

{
"error": {
"code": "invalid_email",
"message": "Email format is invalid"
}
}

Solution:

  • Validate email format before submission
  • Ensure email includes @ and domain

3. Missing Required Fields

Error:

{
"error": {
"code": "validation_error",
"message": "Missing required fields",
"details": {
"required": ["first_name", "last_name", "email"]
}
}
}

Solution:

  • Ensure all required fields are provided
  • Required: first_name, last_name, email

4. Constituent Not Found

Error:

{
"error": {
"code": "not_found",
"message": "Constituent with ID 99999 not found"
}
}

Solution:

  • Verify constituent ID is correct
  • Ensure you're using the right organization token
  • Check if constituent was deleted

Error Handling Pattern

def safe_create_constituent(constituent_data):
"""Create constituent with comprehensive error handling"""

try:
response = requests.post(
f"{BASE_URL}/constituents/ensure_exists",
headers=headers,
json=constituent_data,
timeout=30
)

if response.status_code in [200, 201]:
return {
"success": True,
"constituent": response.json()
}
elif response.status_code == 400:
error = response.json().get("error", {})
return {
"success": False,
"error": "validation_error",
"message": error.get("message"),
"details": error.get("details")
}
elif response.status_code == 401:
return {
"success": False,
"error": "authentication_error",
"message": "Invalid or expired token"
}
elif response.status_code == 409:
return {
"success": False,
"error": "duplicate",
"message": "Constituent already exists"
}
else:
return {
"success": False,
"error": "unknown_error",
"message": f"Unexpected status code: {response.status_code}",
"response": response.text
}

except requests.exceptions.Timeout:
return {
"success": False,
"error": "timeout",
"message": "Request timed out after 30 seconds"
}
except requests.exceptions.ConnectionError:
return {
"success": False,
"error": "connection_error",
"message": "Could not connect to Oho API"
}
except Exception as e:
return {
"success": False,
"error": "exception",
"message": str(e)
}

# Usage
result = safe_create_constituent({
"first_name": "Jane",
"last_name": "Doe",
"email": "jane.doe@example.com"
})

if result["success"]:
print(f"Created constituent: {result['constituent']['id']}")
else:
print(f"Error: {result['error']} - {result['message']}")

Best Practices

1. Always Use external_id

Link Oho constituents to your HR system:

# Good - with external_id
{
"first_name": "John",
"last_name": "Smith",
"email": "john.smith@example.com",
"external_id": "EMP-2024-1234" # Your HR system ID
}

# Not ideal - without external_id
{
"first_name": "John",
"last_name": "Smith",
"email": "john.smith@example.com"
# Harder to sync later
}

2. Use ensure_exists for Idempotency

Prefer ensure_exists over direct creation:

# Good - idempotent
response = requests.post(
f"{BASE_URL}/constituents/ensure_exists",
headers=headers,
json=constituent_data
)
# Safe to call multiple times

# Risky - may create duplicates
response = requests.post(
f"{BASE_URL}/constituents", # Direct creation
headers=headers,
json=constituent_data
)

3. Implement Rate Limiting for Bulk Operations

import time

def bulk_create_with_rate_limit(constituents, delay=0.2):
"""Create constituents with rate limiting"""

results = []

for i, constituent in enumerate(constituents):
response = requests.post(
f"{BASE_URL}/constituents/ensure_exists",
headers=headers,
json=constituent
)

results.append(response.json())

# Rate limit: wait between requests
if i < len(constituents) - 1:
time.sleep(delay) # 200ms delay

return results

4. Keep Active Status Current

Update active status when employment changes:

# When employee leaves
requests.patch(
f"{BASE_URL}/constituents/{constituent_id}",
headers=headers,
json={"active": False}
)

# When employee returns
requests.patch(
f"{BASE_URL}/constituents/{constituent_id}",
headers=headers,
json={"active": True}
)

5. Validate Data Before Submission

import re

def validate_constituent_data(data):
"""Validate data before submission"""

errors = []

# Check required fields
required = ["first_name", "last_name", "email"]
for field in required:
if not data.get(field):
errors.append(f"Missing required field: {field}")

# Validate email format
email = data.get("email", "")
if email and not re.match(r"[^@]+@[^@]+\.[^@]+", email):
errors.append(f"Invalid email format: {email}")

# Validate date_of_birth format (YYYY-MM-DD)
dob = data.get("date_of_birth")
if dob and not re.match(r"\d{4}-\d{2}-\d{2}", dob):
errors.append(f"Invalid date_of_birth format: {dob} (expected YYYY-MM-DD)")

return {
"valid": len(errors) == 0,
"errors": errors
}

# Usage
validation = validate_constituent_data({
"first_name": "John",
"last_name": "Smith",
"email": "invalid-email"
})

if validation["valid"]:
# Proceed with creation
pass
else:
print("Validation errors:")
for error in validation["errors"]:
print(f" - {error}")

6. Create Constituents Before Checks

Always create constituent records before submitting checks:

# Good workflow
constituent = create_constituent(employee_data)
submit_wwc_check(constituent["id"], wwc_data)
submit_ndis_check(constituent["id"], ndis_data)

# Not ideal workflow
submit_wwc_check(None, wwc_data) # Standalone check
# Harder to track and manage

7. Implement Logging and Monitoring

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def create_constituent_with_logging(data):
"""Create constituent with detailed logging"""

logger.info(f"Creating constituent: {data.get('email')}")

try:
response = requests.post(
f"{BASE_URL}/constituents/ensure_exists",
headers=headers,
json=data,
timeout=30
)

if response.status_code in [200, 201]:
constituent = response.json()
logger.info(f"✓ Created constituent {constituent['id']} for {data.get('email')}")
return constituent
else:
logger.error(f"✗ Failed to create constituent: {response.status_code} - {response.text}")
return None

except Exception as e:
logger.exception(f"✗ Exception creating constituent: {e}")
return None

FAQ

How do I handle employees who work in multiple states?

Create one constituent and submit multiple state-specific checks linked to that constituent:

# Create constituent once
constituent = create_constituent(employee_data)

# Submit checks for all relevant states
submit_check(constituent["id"], "vicwwc", vic_card_number)
submit_check(constituent["id"], "nswwwc", nsw_card_number)

Can I update a constituent's email address?

Yes, use PATCH to update the email:

requests.patch(
f"{BASE_URL}/constituents/{constituent_id}",
headers=headers,
json={"email": "newemail@example.com"}
)

What happens if I delete a constituent?

Deleting a constituent sets active: false but preserves the record and linked accreditations for audit purposes.

How do I handle seasonal employees who return each year?

Keep the constituent record and update the active status:

# Start of season
requests.patch(
f"{BASE_URL}/constituents/{constituent_id}",
headers=headers,
json={"active": True}
)

# End of season
requests.patch(
f"{BASE_URL}/constituents/{constituent_id}",
headers=headers,
json={"active": False}
)

Can I bulk export all constituents?

Yes, use GET /constituents to retrieve all records:

response = requests.get(
f"{BASE_URL}/constituents",
headers=headers
)
constituents = response.json()

# Export to CSV
import csv

with open('constituents.csv', 'w', newline='') as csvfile:
fieldnames = ['id', 'first_name', 'last_name', 'email', 'external_id', 'active']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)

writer.writeheader()
for constituent in constituents:
writer.writerow({
'id': constituent['id'],
'first_name': constituent['first_name'],
'last_name': constituent['last_name'],
'email': constituent['email'],
'external_id': constituent.get('external_id'),
'active': constituent['active']
})

How often should I sync with my HR system?

Recommendations:

  • Real-time: Use webhooks from HR system for immediate updates (new hires, terminations)
  • Batch: Daily sync for bulk updates (contact info changes, status updates)
  • Hybrid: Real-time for critical events, daily sync for everything else

What's the rate limit for constituent creation?

Standard API rate limits apply. For bulk operations:

  • Implement 100-200ms delays between requests
  • Use exponential backoff for retries
  • Contact support for higher limits if needed


Version: 1.0 Last Updated: 2026-01-24 Status: Production Ready