Skip to content

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

  1. Overview
  2. Authentication
  3. Error Handling
  4. Rate Limits
  5. Endpoints
  6. Health & System
  7. Authentication
  8. Users
  9. Persons (Contacts)
  10. Tags
  11. Events
  12. Segments
  13. i360 Voter Database
  14. AI Chat (CampaignBrain)
  15. Conversations
  16. Surveys
  17. Work Queue
  18. Communications
  19. Files
  20. Branding
  21. 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:

Authorization: Bearer <access_token>

Token Acquisition:

POST /api/auth/token
Content-Type: application/x-www-form-urlencoded

username=<username>&password=<password>

Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer"
}

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:

X-API-Key: <api_key>

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

{
  "detail": "A person with this email already exists"
}

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:

{
  "name": "CampaignBrain",
  "version": "0.1.0",
  "status": "online"
}


GET /api/health

Health check endpoint. No authentication required.

Response:

{
  "status": "healthy"
}


GET /api/protected

Test endpoint for authentication verification.

Authentication: Required

Response:

{
  "message": "Hello, John!",
  "user_id": "uuid-string",
  "role": "admin"
}


Authentication Endpoints

POST /api/auth/token

Authenticate user and obtain access token.

Request:

Content-Type: application/x-www-form-urlencoded

username=admin&password=password123

Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer"
}

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:

{
  "email": "newemail@example.com",
  "first_name": "Johnny",
  "role": "director"
}

Response: Updated user object


DELETE /api/users/{user_id}

Delete a user record.

Response:

{
  "message": "User deleted successfully"
}

Errors: - 400: Cannot delete your own account - 404: User not found


POST /api/users/{user_id}/password

Change a user's password.

Request Body:

{
  "password": "newSecurePass456"
}

Response:

{
  "message": "Password updated successfully"
}


POST /api/users/{user_id}/toggle-active

Toggle a user's active status.

Response:

{
  "message": "User deactivated successfully"
}

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:

{
  "message": "Person deleted successfully"
}


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:

[
  {"id": "uuid", "name": "VIP", "category": "Priority", "description": "..."}
]


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:

["tag-uuid-1", "tag-uuid-2"]

Response:

{
  "added": 2,
  "skipped": 0,
  "total": 2
}


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:

{
  "person_id": "person-uuid",
  "status": "registered",
  "payment_id": null
}

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:

{
  "name": "Voter Issues Survey",
  "description": "Survey about key campaign issues"
}


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:

{
  "survey_id": "survey-uuid",
  "display_order": 1,
  "is_required": true
}


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:

{
  "reason": "Wrong number"
}


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:

{
  "primary_color": "#DC2626",
  "app_name": "My Campaign"
}


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

Real-time audience search WebSocket.

Connection:

const ws = new WebSocket('wss://tenant.nominate.ai/api/ws/audience-search');

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