1138 lines
26 KiB
Markdown

# 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**:
```javascript
// 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**:
```typescript
// 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**:
```typescript
// 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
```javascript
// 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
```typescript
// 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**:
```json
{
"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**:
```json
{
"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
```javascript
// Lookup representatives by postal code
POST /representatives/lookup
Body: { postalCode: "M5V 1A1" }
// List cached representatives (admin)
GET /admin/representatives
```
#### V2 Representative Endpoints
```typescript
// 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
```javascript
// Send campaign email
POST /campaigns/:id/send-email
Body: { senderName, senderEmail, postalCode }
```
#### V2 Email Endpoints
```typescript
// 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)
```typescript
// 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
```javascript
// Submit response
POST /responses/submit
Body: { campaignId, name, email, message }
// List responses
GET /responses/:campaignId
```
#### V2 Response Endpoints
```typescript
// 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
```javascript
// 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
```typescript
// 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
```javascript
// List cuts (admin)
GET /admin/cuts
// Create cut (admin)
POST /admin/cuts/create
Body: { Name, GeoJSON }
```
#### V2 Cut Endpoints
```typescript
// 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
```javascript
// 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
```typescript
// 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)
```typescript
// 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)
```typescript
// 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
```typescript
// 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
```typescript
// 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
```json
{
"success": true,
"data": { /* response data */ }
}
```
### Paginated Response
```json
{
"success": true,
"data": [ /* items */ ],
"pagination": {
"page": 1,
"limit": 20,
"total": 100,
"totalPages": 5
}
}
```
### Error Response
```json
{
"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**:
```javascript
// Fetch campaigns
fetch('/campaigns?page=1', {
credentials: 'include'
})
.then(res => res.json())
.then(data => {
displayCampaigns(data.list);
displayPagination(data.pageInfo);
});
```
**V2 Code**:
```typescript
// 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**:
```javascript
// 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**:
```typescript
// 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
```
## Related Documentation
- [Migration Overview](index.md) - Migration planning
- [Breaking Changes](breaking-changes.md) - V1→V2 differences
- [Data Migration](data-migration.md) - Data transfer guide
- [Authentication](../architecture/authentication.md) - JWT flow details
- [API Reference](../api-reference/index.md) - Full API documentation
## 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).