26 KiB

API Endpoint Changes

This document provides a comprehensive mapping of V1 API endpoints to their V2 equivalents, including request/response format changes, authentication differences, and code migration examples.

Overview

V2 API represents a complete redesign with:

  • RESTful conventions (proper HTTP methods)
  • Unified namespace (single API at /api/*)
  • JWT authentication (Bearer tokens instead of sessions)
  • Zod validation (type-safe request validation)
  • Standardized responses ({ success, data, pagination } structure)

!!! tip "Migration Strategy" Update frontend API calls incrementally, starting with authentication (foundational), then module by module (campaigns, locations, shifts, etc.).

Authentication Changes

V1 Authentication (Session Cookies)

V1 Login:

// POST /auth/login
fetch('http://localhost:3333/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  credentials: 'include', // Send/receive cookies
  body: JSON.stringify({
    email: 'admin@example.com',
    password: 'password123'
  })
});

// Response: 302 Redirect to /dashboard
// Session cookie set automatically

// Subsequent requests
fetch('http://localhost:3333/campaigns', {
  credentials: 'include' // Sends session cookie
});

V2 Authentication (JWT Bearer Tokens)

V2 Login:

// POST /api/auth/login
const response = await fetch('http://localhost:4000/api/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: 'admin@example.com',
    password: 'Admin123!'
  })
});

const data = await response.json();

// Response:
// {
//   "success": true,
//   "data": {
//     "user": {
//       "id": "clx1a2b3c4d5e6f7g8h9i",
//       "email": "admin@example.com",
//       "name": "Admin User",
//       "role": "SUPER_ADMIN"
//     },
//     "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
//     "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
//   }
// }

// Store tokens (localStorage, sessionStorage, or memory)
localStorage.setItem('accessToken', data.data.accessToken);
localStorage.setItem('refreshToken', data.data.refreshToken);

// Subsequent requests
fetch('http://localhost:4000/api/influence/campaigns', {
  headers: {
    'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
  }
});

V2 Token Refresh:

// POST /api/auth/refresh
const response = await fetch('http://localhost:4000/api/auth/refresh', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    refreshToken: localStorage.getItem('refreshToken')
  })
});

const data = await response.json();

// Response:
// {
//   "success": true,
//   "data": {
//     "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
//     "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." // New token (rotation)
//   }
// }

// Update stored tokens
localStorage.setItem('accessToken', data.data.accessToken);
localStorage.setItem('refreshToken', data.data.refreshToken);

Authentication Endpoint Mapping

V1 Endpoint V2 Endpoint Method Changes
/auth/login /api/auth/login POST Returns JWT tokens instead of setting cookie
/auth/logout /api/auth/logout POST Requires refreshToken in body
/auth/register /api/auth/register POST Always creates USER role (no role in request)
/auth/me /api/auth/me GET Returns 401 if invalid (not 404)
- /api/auth/refresh POST New: refresh token rotation

Influence Module API

Campaigns

V1 Campaign Endpoints

// List campaigns
GET /campaigns
Query: ?page=1

// View campaign
GET /campaigns/:id

// Create campaign (admin)
POST /campaigns/create
Body: { Title, Description, Slug, IsActive }

// Update campaign (admin)
POST /campaigns/:id/edit
Body: { Title, Description, Slug, IsActive }

// Delete campaign (admin)
POST /campaigns/:id/delete

V2 Campaign Endpoints

// List campaigns
GET /api/influence/campaigns
Query: ?page=1&limit=20&search=query&active=true&highlighted=false
Auth: Optional (public returns only active campaigns)

// Get campaign by ID
GET /api/influence/campaigns/:id
Auth: Required (admin)

// Get campaign by slug (public)
GET /api/influence/campaigns/public/:slug
Auth: None

// Create campaign
POST /api/influence/campaigns
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
Body: {
  "title": "Save the Trees",
  "description": "Campaign description",
  "slug": "save-the-trees",
  "active": true,
  "highlighted": false,
  "targetLevel": "federal",
  "targetPosition": "MP",
  "responseWallEnabled": true
}

// Update campaign
PUT /api/influence/campaigns/:id
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
Body: { title, description, ... } // Partial update

// Delete campaign
DELETE /api/influence/campaigns/:id
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)

// Toggle active status
PATCH /api/influence/campaigns/:id/toggle-active
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)

// Toggle highlighted status
PATCH /api/influence/campaigns/:id/toggle-highlighted
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)

Campaign Response Format Changes

V1 Response:

{
  "list": [
    {
      "Id": 1,
      "Title": "Save the Trees",
      "Description": "Campaign description",
      "Slug": "save-the-trees",
      "IsActive": true,
      "Created": "2024-01-15T10:30:00Z"
    }
  ],
  "pageInfo": {
    "totalRows": 100,
    "page": 1,
    "pageSize": 20
  }
}

V2 Response:

{
  "success": true,
  "data": [
    {
      "id": "clx1a2b3c4d5e6f7g8h9i",
      "title": "Save the Trees",
      "description": "Campaign description",
      "slug": "save-the-trees",
      "active": true,
      "highlighted": false,
      "targetLevel": "federal",
      "targetPosition": "MP",
      "responseWallEnabled": true,
      "createdAt": "2024-01-15T10:30:00.000Z",
      "updatedAt": "2024-01-15T10:30:00.000Z",
      "createdBy": {
        "id": "clx1a2b3c4d5e6f7g8h9i",
        "name": "Admin User",
        "email": "admin@example.com"
      }
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 100,
    "totalPages": 5
  }
}

Representatives

V1 Representative Endpoints

// Lookup representatives by postal code
POST /representatives/lookup
Body: { postalCode: "M5V 1A1" }

// List cached representatives (admin)
GET /admin/representatives

V2 Representative Endpoints

// Lookup representatives (public)
POST /api/influence/representatives/lookup
Auth: None
Body: { "postalCode": "M5V1A1" }
Response: {
  "success": true,
  "data": [
    {
      "name": "John Doe",
      "email": "john.doe@parl.gc.ca",
      "district": "Toronto Centre",
      "party": "Liberal",
      "level": "federal",
      "photoUrl": "https://..."
    }
  ]
}

// List cached representatives (admin)
GET /api/influence/representatives
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
Query: ?page=1&limit=20&level=federal&party=Liberal&search=John

// Get representative stats (admin)
GET /api/influence/representatives/stats
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
Response: {
  "success": true,
  "data": {
    "total": 338,
    "byLevel": { "federal": 338, "provincial": 124 },
    "byParty": { "Liberal": 159, "Conservative": 119, "NDP": 25 }
  }
}

// Get representative by ID (admin)
GET /api/influence/representatives/:id
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)

// Delete representative (admin)
DELETE /api/influence/representatives/:id
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)

// Health check
GET /api/influence/representatives/health
Auth: None

Campaign Emails

V1 Email Endpoints

// Send campaign email
POST /campaigns/:id/send-email
Body: { senderName, senderEmail, postalCode }

V2 Email Endpoints

// Send campaign email (public)
POST /api/influence/campaign-emails/send-email
Auth: None
Rate Limit: 30 requests/hour per IP
Body: {
  "campaignId": "clx1a2b3c4d5e6f7g8h9i",
  "postalCode": "M5V1A1",
  "senderName": "Jane Doe",
  "senderEmail": "jane@example.com",
  "customMessage": "Optional custom message"
}

// Track mailto clicks (public)
GET /api/influence/campaign-emails/track-mailto/:emailId
Auth: None

// List campaign emails (admin)
GET /api/influence/campaign-emails
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
Query: ?campaignId=xxx&page=1&limit=20&sortBy=createdAt&sortOrder=desc

// Get campaign email stats (admin)
GET /api/influence/campaign-emails/stats/:campaignId
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
Response: {
  "success": true,
  "data": {
    "totalEmails": 1234,
    "queuedEmails": 5,
    "sentEmails": 1200,
    "failedEmails": 29,
    "mailtoClicks": 340
  }
}

Email Queue

V2 Email Queue Endpoints (New)

// Get queue stats (admin)
GET /api/influence/email-queue/stats
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
Response: {
  "success": true,
  "data": {
    "waiting": 10,
    "active": 2,
    "completed": 5000,
    "failed": 15,
    "delayed": 0,
    "paused": false
  }
}

// Pause queue (admin)
POST /api/influence/email-queue/pause
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)

// Resume queue (admin)
POST /api/influence/email-queue/resume
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)

// Clean completed jobs (admin)
POST /api/influence/email-queue/clean
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
Query: ?grace=3600 (seconds)

// Retry failed jobs (admin)
POST /api/influence/email-queue/retry-failed
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)

Response Wall

V1 Response Endpoints

// Submit response
POST /responses/submit
Body: { campaignId, name, email, message }

// List responses
GET /responses/:campaignId

V2 Response Endpoints

// Submit response (public)
POST /api/influence/responses/submit
Auth: None
Body: {
  "campaignId": "clx1a2b3c4d5e6f7g8h9i",
  "name": "Jane Doe",
  "email": "jane@example.com",
  "message": "I support this campaign!",
  "ipAddress": "192.168.1.1" // Auto-captured by server
}
// Sends verification email

// Verify response email
GET /api/influence/responses/verify/:token
Auth: None

// List responses (public)
GET /api/influence/responses/campaign/:campaignId
Auth: None
Query: ?page=1&limit=20&sortBy=upvotes&sortOrder=desc
Response: Only returns APPROVED responses

// Upvote response (public)
POST /api/influence/responses/:id/upvote
Auth: Optional (tracks by IP + userId if logged in)
Body: { "ipAddress": "192.168.1.1" }

// List responses (admin)
GET /api/influence/responses
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
Query: ?page=1&limit=20&campaignId=xxx&status=PENDING&sortBy=createdAt&sortOrder=desc

// Get response detail (admin)
GET /api/influence/responses/:id
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)

// Approve response (admin)
PATCH /api/influence/responses/:id/approve
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)

// Reject response (admin)
PATCH /api/influence/responses/:id/reject
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)

// Delete response (admin)
DELETE /api/influence/responses/:id
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)

Map Module API

Locations

V1 Location Endpoints

// List locations
GET /locations
Query: ?page=1

// Create location (admin)
POST /locations/create
Body: { Address, Latitude, Longitude, SupportLevel, Notes }

// Update location (admin)
POST /locations/:id/edit
Body: { Address, Latitude, Longitude, SupportLevel, Notes }

// Delete location (admin)
POST /locations/:id/delete

V2 Location Endpoints

// List locations (admin)
GET /api/map/locations
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Query: ?page=1&limit=20&search=query&supportLevel=SUPPORT&cutId=xxx&geocoded=true

// List locations (public map)
GET /api/map/locations/public
Auth: None
Query: ?bounds=minLat,minLng,maxLat,maxLng (returns only geocoded locations)

// Get location by ID (admin)
GET /api/map/locations/:id
Auth: Required (SUPER_ADMIN, MAP_ADMIN)

// Create location (admin)
POST /api/map/locations
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Body: {
  "address": "123 Main St",
  "city": "Toronto",
  "province": "ON",
  "postalCode": "M5V1A1",
  "country": "Canada",
  "latitude": 43.6532,
  "longitude": -79.3832,
  "supportLevel": "SUPPORT",
  "notes": "Spoke with resident",
  "contactName": "John Doe",
  "contactPhone": "416-555-1234",
  "contactEmail": "john@example.com",
  "cutId": "clx1a2b3c4d5e6f7g8h9i"
}

// Update location (admin)
PUT /api/map/locations/:id
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Body: { address, city, ... } // Partial update

// Delete location (admin)
DELETE /api/map/locations/:id
Auth: Required (SUPER_ADMIN, MAP_ADMIN)

// Bulk delete locations (admin)
POST /api/map/locations/bulk-delete
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Body: { "ids": ["id1", "id2", "id3"] }

// Export locations CSV (admin)
GET /api/map/locations/export
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Query: ?supportLevel=SUPPORT&cutId=xxx

// Import locations CSV (admin)
POST /api/map/locations/import
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Content-Type: multipart/form-data
Body: FormData with CSV file

// Geocode location (admin)
POST /api/map/locations/:id/geocode
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Query: ?provider=nominatim (optional)

// Bulk geocode (admin)
POST /api/map/locations/bulk-geocode
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Query: ?limit=100&provider=nominatim

// Reverse geocode (admin)
POST /api/map/locations/reverse-geocode
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Body: { "latitude": 43.6532, "longitude": -79.3832 }

// Get location stats (admin)
GET /api/map/locations/stats
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Response: {
  "success": true,
  "data": {
    "total": 10000,
    "geocoded": 9500,
    "notGeocoded": 500,
    "bySupportLevel": {
      "STRONG_SUPPORT": 1200,
      "SUPPORT": 3400,
      "UNDECIDED": 2100,
      "OPPOSED": 1800,
      "STRONG_OPPOSED": 800,
      "UNKNOWN": 700
    }
  }
}

// NAR Import (admin, new in V2)
GET /api/map/locations/nar/datasets
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Response: List of available NAR datasets (provinces)

POST /api/map/locations/nar/import
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Body: {
  "province": "24",
  "cityFilter": "Toronto",
  "postalCodeFilter": "M5V",
  "cutId": "clx1a2b3c4d5e6f7g8h9i",
  "residentialOnly": true
}

Cuts (Territories)

V1 Cut Endpoints

// List cuts (admin)
GET /admin/cuts

// Create cut (admin)
POST /admin/cuts/create
Body: { Name, GeoJSON }

V2 Cut Endpoints

// List cuts (admin)
GET /api/map/cuts
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Query: ?page=1&limit=20&search=query

// List cuts (public map)
GET /api/map/cuts/public
Auth: None
Response: Only returns active cuts with GeoJSON

// Get cut by ID (admin)
GET /api/map/cuts/:id
Auth: Required (SUPER_ADMIN, MAP_ADMIN)

// Create cut (admin)
POST /api/map/cuts
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Body: {
  "name": "Downtown Toronto",
  "description": "Downtown canvassing area",
  "color": "#FF5733",
  "coordinates": [[[-79.4, 43.6], [-79.3, 43.6], ...]]
}

// Update cut (admin)
PUT /api/map/cuts/:id
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Body: { name, description, color, coordinates }

// Delete cut (admin)
DELETE /api/map/cuts/:id
Auth: Required (SUPER_ADMIN, MAP_ADMIN)

// Get locations in cut (admin)
GET /api/map/cuts/:id/locations
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Query: ?page=1&limit=20

Shifts

V1 Shift Endpoints

// List shifts
GET /shifts

// Create shift (admin)
POST /shifts/create
Body: { Name, StartTime, EndTime, Location, Capacity }

// Signup for shift
POST /shifts/:id/signup
Body: { name, email, phone }

V2 Shift Endpoints

// List shifts (public)
GET /api/map/shifts/public
Auth: None
Query: ?upcoming=true&startDate=2024-01-01&endDate=2024-12-31
Response: Only returns future shifts with available capacity

// List shifts (admin)
GET /api/map/shifts
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Query: ?page=1&limit=20&startDate=2024-01-01&endDate=2024-12-31&cutId=xxx

// Get shift by ID (admin)
GET /api/map/shifts/:id
Auth: Required (SUPER_ADMIN, MAP_ADMIN)

// Create shift (admin)
POST /api/map/shifts
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Body: {
  "name": "Downtown Canvassing",
  "description": "Canvassing shift for downtown area",
  "startTime": "2024-02-15T09:00:00Z",
  "endTime": "2024-02-15T12:00:00Z",
  "location": "Community Center, 123 Main St",
  "capacity": 20,
  "requirements": "Comfortable shoes, water bottle",
  "cutId": "clx1a2b3c4d5e6f7g8h9i"
}

// Update shift (admin)
PUT /api/map/shifts/:id
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Body: { name, startTime, ... }

// Delete shift (admin)
DELETE /api/map/shifts/:id
Auth: Required (SUPER_ADMIN, MAP_ADMIN)

// Signup for shift (public)
POST /api/map/shifts/:id/signup
Auth: None
Body: {
  "name": "Jane Doe",
  "email": "jane@example.com",
  "phone": "416-555-1234",
  "notes": "First time volunteering"
}
// Creates TEMP user if email doesn't exist, sends confirmation email

// Cancel signup (public)
DELETE /api/map/shifts/:shiftId/signups/:userId
Auth: Optional (user can cancel own signup)

// List signups for shift (admin)
GET /api/map/shifts/:id/signups
Auth: Required (SUPER_ADMIN, MAP_ADMIN)

// Update signup status (admin)
PATCH /api/map/shifts/:shiftId/signups/:userId
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Body: { "status": "COMPLETED" }

// Email all shift signups (admin)
POST /api/map/shifts/:id/email-signups
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Body: {
  "subject": "Shift Reminder",
  "message": "Don't forget about tomorrow's shift!"
}

// Get shift stats (admin)
GET /api/map/shifts/stats
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Response: {
  "success": true,
  "data": {
    "totalShifts": 50,
    "upcomingShifts": 12,
    "totalSignups": 234,
    "signupsByStatus": {
      "CONFIRMED": 200,
      "COMPLETED": 30,
      "CANCELLED": 4
    }
  }
}

Canvassing (New in V2)

// Start canvass session (volunteer)
POST /api/map/canvass/sessions/start
Auth: Required (any authenticated user)
Body: {
  "shiftId": "clx1a2b3c4d5e6f7g8h9i",
  "cutId": "clx1a2b3c4d5e6f7g8h9i"
}

// End canvass session (volunteer)
POST /api/map/canvass/sessions/end
Auth: Required (any authenticated user)
Body: { "sessionId": "clx1a2b3c4d5e6f7g8h9i" }

// Get walking route (volunteer)
GET /api/map/canvass/routes/:cutId
Auth: Required (any authenticated user)
Response: Optimized walking route (nearest-neighbor algorithm)

// Record visit (volunteer)
POST /api/map/canvass/visits
Auth: Required (any authenticated user)
Rate Limit: 30 requests/minute
Body: {
  "sessionId": "clx1a2b3c4d5e6f7g8h9i",
  "locationId": "clx1a2b3c4d5e6f7g8h9i",
  "outcome": "CONTACT_MADE",
  "supportLevel": "SUPPORT",
  "notes": "Very interested in campaign"
}

// Get canvass dashboard stats (admin)
GET /api/map/canvass/dashboard/stats
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Response: {
  "success": true,
  "data": {
    "activeSessions": 5,
    "totalVisitsToday": 234,
    "totalVisitsWeek": 1420,
    "avgVisitsPerSession": 47
  }
}

// Get activity feed (admin)
GET /api/map/canvass/dashboard/activity
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Query: ?limit=50

// Get cut progress (admin)
GET /api/map/canvass/dashboard/cut-progress
Auth: Required (SUPER_ADMIN, MAP_ADMIN)

// Get leaderboard (admin)
GET /api/map/canvass/dashboard/leaderboard
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
Query: ?period=week&limit=10

GPS Tracking (New in V2)

// Start tracking session (volunteer)
POST /api/map/tracking/sessions/start
Auth: Required (any authenticated user)
Body: { "sessionId": "clx1a2b3c4d5e6f7g8h9i" }

// Record GPS point (volunteer)
POST /api/map/tracking/points
Auth: Required (any authenticated user)
Body: {
  "sessionId": "clx1a2b3c4d5e6f7g8h9i",
  "latitude": 43.6532,
  "longitude": -79.3832,
  "accuracy": 10.5,
  "altitude": 120.3,
  "speed": 1.2
}

// End tracking session (volunteer)
POST /api/map/tracking/sessions/end
Auth: Required (any authenticated user)
Body: { "sessionId": "clx1a2b3c4d5e6f7g8h9i" }

// Get tracking session (admin)
GET /api/map/tracking/sessions/:id
Auth: Required (SUPER_ADMIN, MAP_ADMIN)

// Get tracking points (admin)
GET /api/map/tracking/sessions/:id/points
Auth: Required (SUPER_ADMIN, MAP_ADMIN)

Landing Pages & Email Templates (New in V2)

Landing Pages

// List landing pages (admin)
GET /api/pages/admin
Auth: Required (SUPER_ADMIN)
Query: ?page=1&limit=20&search=query

// Get page by ID (admin)
GET /api/pages/admin/:id
Auth: Required (SUPER_ADMIN)

// Create page (admin)
POST /api/pages/admin
Auth: Required (SUPER_ADMIN)
Body: {
  "title": "About Us",
  "slug": "about",
  "content": "<html>...</html>",
  "published": true
}

// Update page (admin)
PUT /api/pages/admin/:id
Auth: Required (SUPER_ADMIN)
Body: { title, slug, content, published }

// Delete page (admin)
DELETE /api/pages/admin/:id
Auth: Required (SUPER_ADMIN)

// Export page to MkDocs (admin)
POST /api/pages/admin/:id/export
Auth: Required (SUPER_ADMIN)
Query: ?format=themed&filename=about.html

// Get page by slug (public)
GET /api/pages/public/:slug
Auth: None
Response: Rendered HTML page

Email Templates

// List templates (admin)
GET /api/email-templates
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
Query: ?page=1&limit=20&category=campaign&published=true

// Get template by ID (admin)
GET /api/email-templates/:id
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)

// Create template (admin)
POST /api/email-templates
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
Body: {
  "name": "Campaign Launch",
  "category": "campaign",
  "subject": "New Campaign: {{campaignTitle}}",
  "htmlBody": "<html>...</html>",
  "textBody": "Plain text version",
  "published": true
}

// Update template (admin)
PUT /api/email-templates/:id
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
Body: { name, subject, htmlBody, ... }

// Publish template version (admin)
POST /api/email-templates/:id/publish
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)

// Send test email (admin)
POST /api/email-templates/:id/test
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
Body: {
  "toEmail": "test@example.com",
  "variables": {
    "campaignTitle": "Save the Trees",
    "userName": "Test User"
  }
}

Response Format Standards

Success Response

{
  "success": true,
  "data": { /* response data */ }
}

Paginated Response

{
  "success": true,
  "data": [ /* items */ ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 100,
    "totalPages": 5
  }
}

Error Response

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      {
        "path": ["email"],
        "message": "Invalid email format"
      }
    ]
  }
}

HTTP Status Codes

Code V1 Usage V2 Usage
200 Success (all responses) Success (GET, PUT, PATCH)
201 - Created (POST)
204 - No Content (DELETE)
400 Validation error Bad Request (validation error)
401 Not logged in Unauthorized (invalid token)
403 - Forbidden (insufficient permissions)
404 Not found Not Found
409 - Conflict (duplicate resource)
422 - Unprocessable Entity (business logic error)
429 - Too Many Requests (rate limit)
500 Server error Internal Server Error

Migration Examples

Example 1: Campaign List Page

V1 Code:

// Fetch campaigns
fetch('/campaigns?page=1', {
  credentials: 'include'
})
  .then(res => res.json())
  .then(data => {
    displayCampaigns(data.list);
    displayPagination(data.pageInfo);
  });

V2 Code:

// Fetch campaigns
const token = localStorage.getItem('accessToken');

fetch('/api/influence/campaigns?page=1&limit=20', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
})
  .then(res => res.json())
  .then(response => {
    if (response.success) {
      displayCampaigns(response.data);
      displayPagination(response.pagination);
    } else {
      handleError(response.error);
    }
  });

Example 2: Location Creation

V1 Code:

// Create location
fetch('/locations/create', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  credentials: 'include',
  body: JSON.stringify({
    Address: '123 Main St, Toronto, ON M5V 1A1',
    Latitude: 43.6532,
    Longitude: -79.3832,
    SupportLevel: 'support',
    Notes: 'Spoke with resident'
  })
});

V2 Code:

// Create location
const token = localStorage.getItem('accessToken');

fetch('/api/map/locations', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`
  },
  body: JSON.stringify({
    address: '123 Main St',
    city: 'Toronto',
    province: 'ON',
    postalCode: 'M5V1A1',
    country: 'Canada',
    latitude: 43.6532,
    longitude: -79.3832,
    supportLevel: 'SUPPORT',
    notes: 'Spoke with resident'
  })
})
  .then(res => res.json())
  .then(response => {
    if (response.success) {
      console.log('Created location:', response.data);
    } else {
      handleError(response.error);
    }
  });

Rate Limiting

V2 adds rate limiting to prevent abuse:

Endpoint Limit Window
/api/auth/login 10 requests 1 minute
/api/auth/register 10 requests 1 minute
/api/influence/campaign-emails/send-email 30 requests 1 hour
/api/map/canvass/visits 30 requests 1 minute

Rate Limit Headers (V2 only):

X-RateLimit-Limit: 10
X-RateLimit-Remaining: 8
X-RateLimit-Reset: 1707835200

Next Steps

  1. Review endpoint mappings for your application's usage
  2. Update API client to use JWT authentication
  3. Migrate endpoints incrementally (auth first, then modules)
  4. Test error handling with new response format
  5. Implement rate limit handling (exponential backoff)

!!! tip "API Testing" Use tools like Postman or Thunder Client to test V2 endpoints before frontend migration. Import the V2 API collection from /docs/postman-collection.json (if available).