CampaignBrain API Documentation¶
Version: 0.4.111
Base URL: https://{tenant}.nominate.ai/api or http://localhost:{port}/api
OpenAPI Spec: /docs (Swagger UI) | /redoc (ReDoc) | /openapi.json
Table of Contents¶
- Overview
- Authentication
- Error Handling
- Rate Limits
- Endpoints
- Health & System
- Authentication
- Users
- Persons (Contacts)
- Tags
- Events
- Segments
- i360 Voter Database
- AI Chat (CampaignBrain)
- Conversations
- Surveys
- Work Queue
- Communications
- Files
- Branding
- Integrations
Overview¶
The CampaignBrain API provides programmatic access to campaign management functionality including:
- Contact Management: CRUD operations for persons/voters with tagging and custom fields
- Voter Database: Search and filter i360 voter file data
- AI Chat: Natural language queries against voter data using Claude
- Field Operations: Work queues, segment assignments, and contact logging
- Surveys: Create and manage surveys with YASP integration
- Event Management: Campaign events with registration tracking
Content Types¶
- Request Body:
application/json - File Uploads:
multipart/form-data - Response:
application/json
Pagination Pattern¶
List endpoints return paginated responses:
{
"items": [...],
"total": 1000,
"page": 1,
"per_page": 50,
"total_pages": 20,
"has_next": true,
"has_prev": false
}
Authentication¶
JWT Bearer Token¶
Most endpoints require a JWT bearer token in the Authorization header.
Header Format:
Token Acquisition:
POST /api/auth/token
Content-Type: application/x-www-form-urlencoded
username=<username>&password=<password>
Response:
Token Expiry: 24 hours (configurable via ACCESS_TOKEN_EXPIRE_MINUTES)
API Key Authentication¶
Service-to-service calls can use API key authentication for batch operations.
Header Format:
Supported Endpoints:
- POST /api/persons/batch
- POST /api/persons/bulk-import
Role-Based Access¶
| Role | Description | Permissions |
|---|---|---|
admin |
Full system access | All operations |
director |
Campaign leadership | All except system config |
field |
Field staff | Work queue, contacts, surveys |
volunteer |
Limited access | Read-only, assigned work |
Error Handling¶
HTTP Status Codes¶
| Code | Description | Usage |
|---|---|---|
200 |
OK | Successful GET, PUT |
201 |
Created | Successful POST (resource created) |
400 |
Bad Request | Invalid input, validation error |
401 |
Unauthorized | Missing/invalid token |
403 |
Forbidden | Insufficient permissions |
404 |
Not Found | Resource doesn't exist |
409 |
Conflict | Duplicate resource |
422 |
Unprocessable Entity | Validation failed |
500 |
Internal Server Error | Server-side error |
502 |
Bad Gateway | External service error (YASP, etc.) |
503 |
Service Unavailable | Database unavailable |
Error Response Format¶
For validation errors:
{
"detail": [
{
"loc": ["body", "email"],
"msg": "value is not a valid email address",
"type": "value_error.email"
}
]
}
Rate Limits¶
No explicit rate limits are enforced. However:
- Batch imports: Max 10,000 records per request
- Segment queries: Max 100,000 SVIDs per analysis
- Search results: Max 1,000 per page
Endpoints¶
Health & System¶
GET /api¶
Root API endpoint. Returns application info.
Response:
GET /api/health¶
Health check endpoint. No authentication required.
Response:
GET /api/protected¶
Test endpoint for authentication verification.
Authentication: Required
Response:
Authentication Endpoints¶
POST /api/auth/token¶
Authenticate user and obtain access token.
Request:
Response:
Errors:
- 401: Incorrect username or password
GET /api/auth/me¶
Get current authenticated user.
Authentication: Required
Response:
{
"id": "488e7825-b5ae-4436-ac69-5527fb95ca2e",
"username": "admin",
"email": "admin@example.com",
"first_name": "John",
"last_name": "Doe",
"role": "admin",
"is_active": true,
"organizations": []
}
Users¶
Base Path: /api/users
Required Role: admin
GET /api/users/¶
List users with optional filtering.
Query Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| skip | int | Offset for pagination (default: 0) |
| limit | int | Max results (default: 100, max: 1000) |
| search | string | Search by username, email, or name |
| role | string | Filter by role |
| is_active | bool | Filter by active status |
Response:
[
{
"id": "uuid",
"username": "jdoe",
"email": "jdoe@example.com",
"first_name": "John",
"last_name": "Doe",
"role": "field",
"is_active": true,
"created_at": "2025-01-01T00:00:00",
"organizations": []
}
]
POST /api/users/¶
Create a new user.
Request Body:
{
"username": "jdoe",
"email": "jdoe@example.com",
"password": "securepass123",
"first_name": "John",
"last_name": "Doe",
"role": "field",
"organization_id": "optional-org-uuid"
}
Response: Created user object
Errors:
- 400: Username or email already exists
GET /api/users/{user_id}¶
Get a single user by ID.
Path Parameters:
- user_id (string, required): UUID of the user
Response: User object
Errors:
- 404: User not found
PUT /api/users/{user_id}¶
Update a user record.
Request Body:
Response: Updated user object
DELETE /api/users/{user_id}¶
Delete a user record.
Response:
Errors:
- 400: Cannot delete your own account
- 404: User not found
POST /api/users/{user_id}/password¶
Change a user's password.
Request Body:
Response:
POST /api/users/{user_id}/toggle-active¶
Toggle a user's active status.
Response:
Errors:
- 400: Cannot deactivate your own account
Persons (Contacts)¶
Base Path: /api/persons
GET /api/persons/¶
List persons with optional filtering.
Query Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| skip | int | Offset (default: 0) |
| limit | int | Max results (default: 50, max: 500) |
| search | string | Search by name or email. Supports tag:"TagName" syntax |
| tag | string | Filter by tag name |
| whip_status | string | Filter by whip status |
| precinct_status | string | Filter by precinct status |
Response:
{
"persons": [
{
"id": "uuid",
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com",
"address1": "123 Main St",
"city": "Detroit",
"state": "MI",
"zip": "48201",
"cell_phone": "313-555-1234",
"whip_status": "Supporter",
"tags": [
{"id": "tag-uuid", "name": "VIP", "category": "Priority"}
],
"custom_fields": {
"Party": "Republican",
"Voter Score": "85"
}
}
],
"total": 1000,
"skip": 0,
"limit": 50,
"has_next": true,
"page": 1,
"total_pages": 20
}
POST /api/persons/¶
Create a new person record.
Request Body:
{
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com",
"address1": "123 Main St",
"city": "Detroit",
"state": "MI",
"zip": "48201",
"cell_phone": "313-555-1234",
"tags": ["VIP", "Volunteer"],
"custom_fields": {
"Party": "Republican"
},
"whip_status": "Unknown",
"import_source": "manual",
"import_filename": null,
"original_data": null
}
Response: Created person object
Errors:
- 400: Email already exists
GET /api/persons/{person_id}¶
Get a single person by ID.
Response: Full person object with tags, custom fields, whip status, and recent communications
PUT /api/persons/{person_id}¶
Update a person record. Supports partial updates.
Request Body:
{
"email": "newemail@example.com",
"whip_status": "Strong Supporter",
"tags": ["VIP", "Donor"],
"custom_fields": {
"Party": "Republican",
"Last Contact": "2025-01-01"
}
}
DELETE /api/persons/{person_id}¶
Delete a person and all related records.
Response:
POST /api/persons/batch¶
Bulk import persons with duplicate detection.
Authentication: JWT or API Key
Request Body:
{
"persons": [
{
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com"
}
],
"batch_id": "import-2025-01-01",
"list_name": "January Volunteers",
"skip_duplicates": true
}
Response:
{
"batch_id": "import-2025-01-01",
"list_name": "January Volunteers",
"total": 100,
"imported": 95,
"duplicates": 5,
"failed": 0,
"errors": [],
"imported_ids": ["uuid1", "uuid2", "..."]
}
Duplicate Detection: By email, cell_phone, or home_phone match
POST /api/persons/bulk-import¶
Direct DuckDB bulk import from CSV file. Much faster than /batch.
Authentication: JWT or API Key
Request Body:
{
"file_path": "/tmp/imports/voters.csv",
"mappings": {
"First Name": "first_name",
"Last Name": "last_name",
"Email Address": "email",
"Phone": "cell_phone"
},
"batch_id": "bulk-2025-01-01",
"list_name": "Voter Import",
"skip_duplicates": true
}
Response:
{
"batch_id": "bulk-2025-01-01",
"list_name": "Voter Import",
"imported": 83000,
"duplicates_skipped": 2000,
"tag_name": "list:Voter Import",
"duration_seconds": 2.45
}
Security: File path must be within /tmp/imports/
Performance: - 1,000 records: <1 second - 83,000 records: ~2 seconds
GET /api/persons/search/semantic¶
Search persons using semantic similarity (embeddings).
Query Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| query | string | Natural language search query |
| limit | int | Max results (default: 10, max: 50) |
Response: List of persons with semantic_score field
Requirements: Ollama service with embeddings model
Tags¶
Base Path: /api/tags
GET /api/tags/¶
List tags with optional filtering.
Query Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| parent_id | string | Filter by parent (empty string = root tags) |
| category | string | Filter by category |
Response:
[
{
"id": "uuid",
"name": "VIP",
"description": "High priority contacts",
"category": "Priority",
"parent_id": null,
"children": [
{"id": "child-uuid", "name": "Donor VIP", "...": "..."}
]
}
]
GET /api/tags/search¶
Search tags by name (autocomplete).
Query Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| q | string | Search query (min 1 char) |
| limit | int | Max results (default: 10, max: 50) |
Response:
GET /api/tags/hierarchy¶
Get complete tag hierarchy with parent-child relationships.
POST /api/tags/¶
Create a new tag.
Request Body:
{
"name": "High Priority",
"description": "Urgent outreach needed",
"category": "Priority",
"parent_id": null
}
Errors:
- 400: Tag name already exists
GET /api/tags/{tag_id}¶
Get a single tag with its children.
PUT /api/tags/{tag_id}¶
Update a tag.
DELETE /api/tags/{tag_id}¶
Delete a tag.
Errors:
- 400: Cannot delete tag with children
- 400: Cannot delete tag in use
GET /api/tags/for/{record_type}/{record_id}¶
Get all tags for a specific record.
Path Parameters:
- record_type: person, event, interaction, patch, communication, import, segment
- record_id: UUID of the record
Response:
[
{
"id": "uuid",
"name": "VIP",
"category": "Priority",
"tagged_at": "2025-01-01T00:00:00",
"tagged_by": "user-uuid"
}
]
POST /api/tags/for/{record_type}/{record_id}¶
Add tags to a record. Idempotent.
Request Body:
Response:
DELETE /api/tags/for/{record_type}/{record_id}/{tag_id}¶
Remove a tag from a record.
GET /api/tags/{tag_id}/records¶
Get all records with a specific tag.
Query Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| record_type | string | Filter by record type |
| limit | int | Max results (default: 100) |
| offset | int | Pagination offset |
GET /api/tags/stats/usage¶
Get tag usage statistics.
Response:
{
"total_tags": 50,
"total_associations": 5000,
"by_record_type": [
{"record_type": "person", "count": 4500}
],
"by_category": [
{"category": "Priority", "record_count": 1000, "association_count": 1500}
],
"top_tags": [
{"id": "uuid", "name": "VIP", "category": "Priority", "usage_count": 500}
]
}
Events¶
Base Path: /api/events
GET /api/events/¶
List events with optional filtering.
Query Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| skip | int | Offset (default: 0) |
| limit | int | Max results (default: 100) |
| upcoming | bool | Filter for future events only |
Response: List of events with registrations
POST /api/events/¶
Create a new event.
Request Body:
{
"title": "Campaign Rally",
"description": "Join us for our biggest rally yet",
"location": "Detroit Convention Center",
"start_datetime": "2025-02-15T18:00:00",
"end_datetime": "2025-02-15T21:00:00",
"is_paid": false,
"ticket_price": null,
"max_capacity": 500
}
Errors:
- 400: End time must be after start time
GET /api/events/{event_id}¶
Get a single event with registrations.
PUT /api/events/{event_id}¶
Update an event.
DELETE /api/events/{event_id}¶
Delete an event and all registrations.
POST /api/events/{event_id}/registrations¶
Register a person for an event.
Request Body:
Errors:
- 400: Person already registered
- 400: Event at maximum capacity
- 404: Event or person not found
PUT /api/events/{event_id}/registrations/{registration_id}¶
Update a registration.
DELETE /api/events/{event_id}/registrations/{registration_id}¶
Cancel a registration.
Segments¶
Base Path: /api/segments
Segments are saved queries that define voter audiences for targeting.
GET /api/segments/¶
List all segments with statistics.
Query Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| skip | int | Offset (default: 0) |
| limit | int | Max results (default: 100) |
| is_active | bool | Filter by active status |
| search | string | Search name/description |
Response:
[
{
"id": "uuid",
"name": "High Turnout Republicans",
"description": "Republicans with turnout score > 80",
"natural_language_query": "Find Republicans with high turnout",
"filter_criteria": {
"source": "i360",
"filters": {
"party": ["Republican"],
"turnout_score_min": 80
}
},
"person_count": 15000,
"is_active": true,
"is_dynamic": true,
"total_executions": 10,
"avg_execution_time_ms": 250,
"last_execution": {...}
}
]
POST /api/segments/¶
Create a new segment.
Request Body:
{
"name": "Young Voters",
"description": "Voters under 35",
"natural_language_query": "Find voters under 35",
"is_active": true,
"is_dynamic": true
}
GET /api/segments/{segment_id}¶
Get a segment with statistics.
PUT /api/segments/{segment_id}¶
Update a segment.
DELETE /api/segments/{segment_id}¶
Delete a segment.
POST /api/segments/{segment_id}/execute¶
Execute segment query and update person count.
Response:
{
"message": "Segment executed successfully. Found 15000 matching voters.",
"segment_id": "uuid",
"success": true,
"person_count": 15000,
"execution_time_ms": 250
}
GET /api/segments/{segment_id}/persons¶
Get persons in a segment (static segments only).
GET /api/segments/{segment_id}/geojson¶
Get segment voters as GeoJSON for map visualization.
Query Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| limit | int | Max points (default: 5000, max: 10000) |
Response:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [-83.1, 42.3]},
"properties": {
"svid": 12345,
"name": "John Doe",
"city": "Detroit",
"party": "Republican"
}
}
],
"properties": {
"segment_id": "uuid",
"segment_name": "High Turnout Republicans",
"total_count": 15000,
"returned_count": 5000,
"has_more": true
}
}
i360 Voter Database¶
Base Path: /api/i360
Search and filter the i360 voter file database.
GET /api/i360/search¶
Search voters with advanced filtering.
Query Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| q | string | Text search (name, city, phone, or SVID) |
| gender | string | Filter by gender |
| ethnicity | string | Filter by ethnicity |
| religion | string | Filter by religion |
| birth_year_min | int | Minimum birth year |
| birth_year_max | int | Maximum birth year |
| turnout_min | float | Min turnout score (0-100) |
| turnout_max | float | Max turnout score (0-100) |
| trump_min | float | Min Trump support score |
| trump_max | float | Max Trump support score |
| county | string | Filter by county |
| city | string | Filter by city |
| congressional_district | int | Filter by CD |
| lat | float | Latitude for radius search |
| lng | float | Longitude for radius search |
| radius | float | Search radius in miles |
| voted_2020 | bool | Voted in 2020 general |
| voted_2022 | bool | Voted in 2022 general |
| has_cell | bool | Has cell phone |
| has_email | bool | Has email address |
| page | int | Page number (default: 1) |
| per_page | int | Results per page (default: 25, max: 1000) |
| sort_by | string | Sort field |
| sort_order | string | asc or desc |
Response:
{
"voters": [
{
"svid": 12345678,
"first_name": "John",
"last_name": "Smith",
"city": "Detroit",
"county": "Wayne",
"state": "MI",
"zip_code": "48201",
"party": "Republican",
"gender": "M",
"birth_year": 1975,
"turnout_score": 85.5,
"trump_support_score": 72.3,
"cell_phone": "313-555-1234",
"email_mydata": "john@example.com",
"congressional_district": 13,
"latitude": 42.3314,
"longitude": -83.0458
}
],
"total_count": 50000,
"page": 1,
"per_page": 25,
"total_pages": 2000,
"has_next": true,
"has_prev": false
}
POST /api/i360/search¶
Advanced search with POST body (same filters as GET).
GET /api/i360/filters¶
Get available filter options for search UI.
Response:
{
"counties": [
{"value": "Wayne", "count": 50000}
],
"cities": [
{"value": "Detroit", "count": 30000}
],
"ethnicities": [...],
"religions": [...],
"congressional_districts": [
{"value": 13, "count": 40000}
],
"score_ranges": {
"turnout": {"min": 0, "max": 100},
"trump_support": {"min": 0, "max": 100}
},
"birth_year_range": {"min": 1920, "max": 2006},
"total_voters": 81232
}
GET /api/i360/voter/{svid}¶
Get detailed voter record by SVID.
Response: Full voter object with all fields
GET /api/i360/stats¶
Get aggregate database statistics.
GET /api/i360/geojson¶
Get voters as GeoJSON for map display.
Query Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| limit | int | Max points (default: 1000) |
| Additional filter params... | | Same as search |
AI Chat (CampaignBrain)¶
Base Path: /api/cb-chat
Natural language interface to voter data using Claude.
POST /api/cb-chat/message¶
Process a natural language query.
Request Body:
{
"message": "How many Republicans have a turnout score above 80?",
"history": [
{"role": "user", "content": "previous message"},
{"role": "assistant", "content": "previous response"}
],
"conversation_id": "optional-existing-conversation-uuid",
"debug": false
}
Response:
{
"message": "I found 15,234 Republicans with turnout scores above 80.\n\nShowing first 10 results:",
"type": "results",
"data": {
"count": 15234,
"samples": [...],
"intent": "count"
},
"actions": [
{"label": "Save as Segment", "action": "save_segment", "data": {...}},
{"label": "View in Search", "action": "view_search", "data": {...}}
],
"conversation_id": "uuid",
"share_id": "ABC12345",
"sql": "SELECT COUNT(*) FROM i360_voters WHERE...",
"debug": null
}
Response Types:
- results: Query executed, data returned
- clarify: Need more information (with questions array)
- error: Something went wrong
GET /api/cb-chat/health¶
Check chat system health.
Response:
{
"status": "healthy",
"anthropic_configured": true,
"model": "claude-sonnet-4-20250514",
"data_dictionary_loaded": true,
"query_engine_available": true,
"campaign_data_available": true
}
GET /api/cb-chat/warmup¶
Run warmup health checks before chat session.
Response:
{
"all_passed": true,
"checks": [
{"name": "claude_api", "status": "pass", "message": "Connected", "latency_ms": 150}
],
"total_latency_ms": 500
}
GET /api/cb-chat/suggestions¶
Get suggested queries for the chat UI.
Response:
{
"suggestions": [
{"text": "How many voters do we have?", "type": "count"},
{"text": "Find Republicans with high turnout scores", "type": "list"},
{"text": "What campaign data do we have?", "type": "campaign"}
]
}
GET /api/cb-chat/datasets¶
Get available datasets for querying.
Response:
{
"datasets": [
{
"id": "voters",
"name": "Voter File",
"description": "i360 voter registration and scoring data",
"icon": "users",
"color": "blue",
"available": true,
"fields": ["name", "address", "party", "turnout_score"]
},
{
"id": "campaign",
"name": "Campaign Data",
"description": "Email and donation engagement data",
"available": true
}
]
}
Conversations¶
Base Path: /api/conversations
Manage AI chat conversation history.
GET /api/conversations/¶
List user's conversations.
Query Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| limit | int | Max results (default: 20) |
| offset | int | Pagination offset |
GET /api/conversations/{conversation_id}¶
Get a conversation with messages.
GET /api/conversations/share/{share_id}¶
Get a shared conversation by share ID (no auth required for reading).
DELETE /api/conversations/{conversation_id}¶
Delete a conversation.
Surveys¶
Base Path: /api/surveys
Manage YASP surveys and segment associations.
GET /api/surveys/¶
List all surveys.
Query Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| state | string | Filter: DRAFT, ACTIVE, INACTIVE, ARCHIVED |
POST /api/surveys/¶
Create a new survey.
Required Role: admin or director
Request Body:
GET /api/surveys/{survey_id}¶
Get survey with questions.
PUT /api/surveys/{survey_id}¶
Update survey name/description.
POST /api/surveys/{survey_id}/publish¶
Publish a survey (DRAFT -> ACTIVE).
POST /api/surveys/{survey_id}/pause¶
Pause a survey (ACTIVE -> INACTIVE).
POST /api/surveys/{survey_id}/resume¶
Resume a survey (INACTIVE -> ACTIVE).
POST /api/surveys/{survey_id}/archive¶
Archive a survey (terminal state).
POST /api/surveys/{survey_id}/questions¶
Add a question to a survey.
Request Body:
{
"type": "multiple_choice",
"text": "What issues matter most to you?",
"options": ["Economy", "Healthcare", "Education", "Environment"],
"required": true
}
PUT /api/surveys/{survey_id}/questions/{question_id}¶
Update a question.
DELETE /api/surveys/{survey_id}/questions/{question_id}¶
Delete a question.
POST /api/surveys/responses¶
Submit a survey response.
Request Body:
{
"survey_id": "survey-uuid",
"person_id": "person-uuid",
"answers": {
"question-uuid-1": "Economy",
"question-uuid-2": "Very concerned"
},
"assignment_id": "optional",
"queue_item_id": "optional"
}
GET /api/surveys/segment/{segment_id}¶
Get surveys linked to a segment.
POST /api/surveys/segment/{segment_id}¶
Link a survey to a segment.
Request Body:
Work Queue¶
Base Path: /api/work-queue
Field user workflow for working through assigned contacts.
GET /api/work-queue/my-assignments¶
Get current user's active segment assignments.
Response:
{
"assignments": [
{
"id": "assignment-uuid",
"segment_id": "segment-uuid",
"segment_name": "High Priority Voters",
"priority": 1,
"status": "active",
"total": 500,
"completed": 150,
"remaining": 350
}
]
}
GET /api/work-queue/{assignment_id}/next¶
Get the next contact in the work queue.
Response:
{
"complete": false,
"segment_name": "High Priority Voters",
"assignment_id": "uuid",
"queue_item_id": "uuid",
"position": 151,
"person": {
"id": "person-uuid",
"first_name": "John",
"last_name": "Doe",
"address1": "123 Main St",
"cell_phone": "313-555-1234",
"tags": [...]
},
"history": [
{"action_type": "Call", "result": "No Answer", "created_at": "..."}
],
"stats": {
"total": 500,
"completed": 150,
"pending": 350,
"current_position": 151
},
"surveys": [...]
}
POST /api/work-queue/{queue_item_id}/action¶
Log an action on a contact.
Request Body:
{
"action_type_id": "uuid",
"action_result_id": "uuid",
"notes": "Left voicemail",
"duration_seconds": 30
}
POST /api/work-queue/{queue_item_id}/complete¶
Mark queue item as completed.
POST /api/work-queue/{queue_item_id}/skip¶
Skip a queue item.
Request Body:
GET /api/work-queue/action-types¶
Get available action types with valid results.
Response:
{
"action_types": [
{
"id": "uuid",
"name": "call",
"display_name": "Phone Call",
"icon": "phone",
"results": [
{"id": "uuid", "name": "answered", "display_name": "Answered", "category": "contact"},
{"id": "uuid", "name": "no_answer", "display_name": "No Answer", "category": "no_contact"}
]
}
]
}
GET /api/work-queue/stats/today¶
Get today's activity stats for current user.
Response:
{
"today_calls": 25,
"today_texts": 10,
"today_emails": 5,
"today_doors": 0,
"today_total": 40,
"today_completed": 35
}
GET /api/work-queue/all-queues¶
Get all active work queues (for supervisors).
Communications¶
Base Path: /api/communications
Manage communication logs and templates.
GET /api/communications/¶
List communications with filtering.
Query Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| person_id | string | Filter by person |
| type | string | Filter: email, sms, call |
| status | string | Filter: pending, sent, failed |
POST /api/communications/¶
Create a communication record.
GET /api/communications/{comm_id}¶
Get communication details.
Files¶
Base Path: /api/files
File storage integration with CBFiles service.
GET /api/files/¶
List files for current user.
POST /api/files/upload¶
Upload a file.
Request: multipart/form-data with file field
GET /api/files/{file_id}¶
Get file metadata.
GET /api/files/{file_id}/download¶
Download a file.
DELETE /api/files/{file_id}¶
Delete a file.
Branding¶
Base Path: /api/branding
Manage campaign branding and theming.
GET /api/branding/¶
Get current branding settings.
Response:
{
"primary_color": "#DC2626",
"secondary_color": "#1E40AF",
"accent_color": "#059669",
"text_color": "#374151",
"sidebar_color": "#f8fafc",
"app_name": "CampaignBrain",
"tagline": "Your campaign, organized",
"logos": {
"logo_full": "/static/images/logo-full.png?v=1234567890",
"logo_name": "/static/images/logo-name.png?v=1234567890",
"logo_icon": "/static/images/logo-icon.png?v=1234567890"
}
}
PUT /api/branding/¶
Update branding settings (colors, text).
Request Body:
POST /api/branding/logo/{type}¶
Upload a logo image.
Path Parameters:
- type: logo_full, logo_name, logo_cropped, logo_icon, logo_mobile
Request: multipart/form-data with file field
Accepted Types: PNG, JPG, SVG, WebP (max 5MB)
DELETE /api/branding/logo/{type}¶
Reset a logo to default.
Integrations¶
Base Path: /api/integrations
Manage external service integrations.
GET /api/integrations/¶
List configured integrations.
POST /api/integrations/¶
Configure a new integration.
GET /api/integrations/{integration_id}¶
Get integration details.
PUT /api/integrations/{integration_id}¶
Update integration configuration.
DELETE /api/integrations/{integration_id}¶
Remove an integration.
WebSocket Endpoints¶
WS /api/ws/audience-search¶
Real-time audience search WebSocket.
Connection:
Messages:
// Incoming (from server)
{"type": "connection", "message": "Connected to audience search WebSocket"}
// Outgoing (to server)
{"action": "search", "filters": {...}}
// Incoming (search results)
{"type": "search_results", "data": {"persons": [...], "total": 1000}}
Data Models¶
Person¶
interface Person {
id: string; // UUID
first_name: string;
last_name: string;
address1?: string;
address2?: string;
city?: string;
state?: string; // 2-letter code
zip?: string;
county?: string;
congressional_district?: string;
home_phone?: string;
cell_phone?: string;
email?: string;
precinct?: string;
precinct_number?: string;
precinct_status?: string;
precinct_role?: string;
notes?: string;
whip_status: string; // "Unknown", "Supporter", "Strong Supporter", etc.
tags: Tag[];
custom_fields: Record<string, string>;
import_source?: string;
import_date?: string;
import_batch_id?: string;
import_filename?: string;
created_at: string; // ISO 8601
updated_at: string;
created_by: string;
updated_by: string;
}
Tag¶
interface Tag {
id: string;
name: string;
description?: string;
category?: string; // "Priority", "Status", "Import", etc.
parent_id?: string;
children?: Tag[];
created_at: string;
created_by: string;
}
User¶
interface User {
id: string;
username: string;
email: string;
first_name: string;
last_name: string;
role: "admin" | "director" | "field" | "volunteer";
is_active: boolean;
organizations: Organization[];
created_at: string;
updated_at: string;
}
i360 Voter¶
interface Voter {
svid: number; // State Voter ID
first_name: string;
last_name: string;
city?: string;
county?: string;
state?: string;
zip_code?: string;
party?: string; // "Republican", "Democrat", "Independent", etc.
gender?: string; // "M", "F"
birth_year?: number;
turnout_score?: number; // 0-100
trump_support_score?: number; // 0-100
biden_oppose_score?: number; // 0-100
cell_phone?: string;
email_mydata?: string;
congressional_district?: number;
latitude?: number;
longitude?: number;
}
Changelog¶
v0.4.111¶
- Remove client logos from git tracking
- Branding logos excluded from version control
v0.4.110¶
- Python 3.10 compatibility fix
- Standardize Python environment across tenants
v0.4.109¶
- AI Chat ecosystem documentation
v0.4.108¶
- Code hygiene cycle
- Ruff formatting and linting
v0.4.107¶
- Branding page token handling fix