Stemwijzer API

Complete Political Compass & Party Matching System

Status: Production Ready | All 52 Endpoints Operational

Last Updated: November 10, 2025

52
API Endpoints
12
Controllers
100%
Complete
Database Ready

Quick Start

Get the API running in 2 simple steps:

# 1. Start the server
php -S localhost:8000 -t public

# 2. Test the API
php test_api.php

# Or visit in browser
http://localhost:8000/api/health

Base URL: http://localhost:8000/api

Key Features

JWT Authentication

Secure token-based authentication with bcrypt password hashing

Role-Based Access

Super admin and party admin permissions

Auto-Generation

Automatic party answers, codes, and admin accounts

Political Compass

Calculate party positions on 2D compass

Party Matching

Find top 3 party matches for users

File Uploads

Profile pictures and party logos with auto-cleanup

Bulk Operations

Transaction-based bulk updates

Statistics

Comprehensive analytics and reporting

Empty Answer Support

Track unanswered questions with empty string state

API Endpoints (52 total)

Tip: Click on any endpoint card to view detailed request/response examples, including authentication requirements and sample data.

1. Authentication (4 endpoints)

POST /auth/login No Auth

Authenticate user and receive JWT token

Request Body
{
  "username": "admin@stemwijzer.nl",
  "password": "admin123"
}
Example Response (200 OK)
{
  "status": "success",
  "message": "Login successful",
  "data": {
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
    "admin": {
      "admin_id": 1,
      "username": "admin@stemwijzer.nl",
      "name": "Super Admin",
      "role": "super_admin",
      "profile_picture_url": null
    },
    "expires_at": "2025-11-11T10:00:00+00:00"
  }
}
POST /auth/register No Auth

Register new admin account

Request Body
{
  "username": "newadmin@stemwijzer.nl",
  "password": "securepassword123",
  "name": "New Admin",
  "role": "admin"  // or "super_admin"
}
Example Response (201 Created)
{
  "status": "success",
  "message": "Registration successful",
  "data": {
    "admin_id": 2,
    "username": "newadmin@stemwijzer.nl",
    "name": "New Admin",
    "role": "admin"
  }
}
POST /auth/logout Auth Required

Invalidate authentication token

Headers
Authorization: Bearer {your_token}
Example Response (200 OK)
{
  "status": "success",
  "message": "Logout successful"
}
GET /auth/validate Auth Required

Validate current token and get expiration info

Headers
Authorization: Bearer {your_token}
Example Response (200 OK)
{
  "status": "success",
  "message": "Token is valid",
  "data": {
    "valid": true,
    "admin_id": 1,
    "username": "admin@stemwijzer.nl",
    "role": "super_admin",
    "expires_at": "2025-11-11T10:00:00+00:00",
    "time_remaining": "23 hours"
  }
}

2. Admin Management (4 endpoints)

GET /admins

List all admins (super admin only)

GET /admins/{admin_id}

Get admin details with associated parties

PUT /admins/{admin_id} Auth Required

Update admin profile (name, picture)

Request Body
{
  "name": "Updated Admin Name",
  "profile_picture_url": "uploads/admins/admin_123.jpg"
}
Example Request
PUT /api/admins/1
Authorization: Bearer {your_token}
Content-Type: application/json

{
  "name": "John Doe Admin"
}
Example Response (200 OK)
{
  "status": "success",
  "message": "Admin updated successfully",
  "data": {
    "admin_id": 1,
    "username": "admin@stemwijzer.nl",
    "name": "John Doe Admin",
    "role": "super_admin",
    "profile_picture_url": "uploads/admins/admin_123.jpg"
  }
}
PUT /admins/{admin_id}/password Auth Required

Change admin password

Request Body
{
  "current_password": "oldpassword123",
  "new_password": "newsecurepassword456"
}
Example Request
PUT /api/admins/1/password
Authorization: Bearer {your_token}
Content-Type: application/json

{
  "current_password": "admin123",
  "new_password": "newpassword123"
}
Example Response (200 OK)
{
  "status": "success",
  "message": "Password updated successfully"
}

3. Party Management (5 endpoints)

GET /parties

List all parties with pagination and sorting

GET /parties/{party_id}

Get party details with admins

POST /parties Super Admin

Create party with auto-generation (answers, admin account)

Request Body
{
  "name": "New Political Party",
  "short_code": "NPP",
  "party_link": "https://newparty.nl",
  "profile_picture_url": "uploads/parties/npp_logo.jpg"
}
Auto-Generated Features
✓ Creates admin account: {short_code}@stemwijzer.nl / password123
✓ Generates party answers with empty ("") for all questions
✓ Creates party-admin link automatically
Example Response (201 Created)
{
  "status": "success",
  "message": "Party created successfully",
  "data": {
    "party_id": 5,
    "name": "New Political Party",
    "short_code": "NPP",
    "admin_account": {
      "username": "NPP@stemwijzer.nl",
      "password": "password123"
    }
  }
}
PUT /parties/{party_id} Auth Required

Update party information

Request Body
{
  "name": "Updated Party Name",
  "short_code": "UPN",
  "party_link": "https://updatedparty.nl",
  "profile_picture_url": "uploads/parties/upn_logo.jpg"
}
Example Request
PUT /api/parties/1
Authorization: Bearer {your_token}
Content-Type: application/json

{
  "name": "Progressive Alliance",
  "party_link": "https://progressive-alliance.nl"
}
Example Response (200 OK)
{
  "status": "success",
  "message": "Party updated successfully",
  "data": {
    "party_id": 1,
    "name": "Progressive Alliance",
    "short_code": "PA",
    "party_link": "https://progressive-alliance.nl"
  }
}
DELETE /parties/{party_id}

Delete party with cascade (super admin only)

4. Category Management (5 endpoints)

GET /categories

List all categories with question counts

GET /categories/{category_id}

Get category details

POST /categories Super Admin

Create new category

Request Body
{
  "name": "Economy",
  "description": "Questions about economic policy"
}
Example Response (201 Created)
{
  "status": "success",
  "message": "Category created successfully",
  "data": {
    "category_id": 3,
    "name": "Economy",
    "description": "Questions about economic policy",
    "question_count": 0
  }
}
PUT /categories/{category_id} Super Admin

Update category

Request Body
{
  "name": "Economic Policy",
  "description": "Updated description about economic matters"
}
Example Response (200 OK)
{
  "status": "success",
  "message": "Category updated successfully",
  "data": {
    "category_id": 3,
    "name": "Economic Policy",
    "description": "Updated description about economic matters"
  }
}
DELETE /categories/{category_id}

Delete category (only if no questions)

5. Question Management (5 endpoints)

GET /questions

List all questions with category names

GET /questions/{question_id}

Get question details

POST /questions Super Admin

Create question with auto-generation (party answers, order)

Request Body
{
  "question_text": "Should taxes be increased?",
  "category_id": 1,
  "agree_direction": "Left",
  "disagree_direction": "Right",
  "order_index": 5
}
Auto-Generated Features
✓ Creates empty ("") party answers for all existing parties
✓ Auto-assigns order_index if not provided
✓ Links question to category
Example Response (201 Created)
{
  "status": "success",
  "message": "Question created successfully",
  "data": {
    "question_id": 15,
    "question_text": "Should taxes be increased?",
    "category_id": 1,
    "agree_direction": "Left",
    "disagree_direction": "Right",
    "order_index": 5,
    "party_answers_created": 12
  }
}
PUT /questions/{question_id} Super Admin

Update question

Request Body
{
  "question_text": "Should income taxes be increased?",
  "category_id": 1,
  "agree_direction": "Left",
  "disagree_direction": "Right",
  "order_index": 3
}
Example Response (200 OK)
{
  "status": "success",
  "message": "Question updated successfully",
  "data": {
    "question_id": 15,
    "question_text": "Should income taxes be increased?",
    "category_id": 1,
    "agree_direction": "Left",
    "disagree_direction": "Right",
    "order_index": 3
  }
}
DELETE /questions/{question_id}

Delete question with cascade

6. Party Answers (5 endpoints)

New Feature: Party answers now support agree, disagree, neutral, and "" (empty string for unanswered questions).
GET /party-answers?party_id={id} Auth Required

Get party answers with filters

Query Parameters
party_id (required): Party ID
question_id (optional): Filter by question
category_id (optional): Filter by category
page (optional): Page number (default: 1)
per_page (optional): Items per page (default: 1000)
Example Request
GET /api/party-answers?party_id=1&category_id=2
Authorization: Bearer {your_token}
Example Response (200 OK)
{
  "status": "success",
  "message": "Success",
  "data": {
    "party_answers": [
      {
        "answer_id": 1,
        "party_id": 1,
        "question_id": 1,
        "answer": "agree",
        "update_counter": 2
      },
      {
        "answer_id": 2,
        "party_id": 1,
        "question_id": 2,
        "answer": "",
        "update_counter": 0
      }
    ]
  }
}
PUT /party-answers/{party_id}/{question_id} Auth Required

Update single party answer

Request Body
{
  "answer": "agree"  // Can be: "agree", "disagree", "neutral", or "" (empty)
}
Example Request
PUT /api/party-answers/1/5
Authorization: Bearer {your_token}
Content-Type: application/json

{
  "answer": ""  // Mark as unanswered
}
Example Response (200 OK)
{
  "status": "success",
  "message": "Answer updated successfully",
  "data": {
    "party_answer": {
      "party_id": 1,
      "question_id": 5,
      "answer": "",
      "update_counter": 3
    }
  }
}
POST /party-answers/bulk Auth Required

Bulk update party answers (transaction-based)

Request Body
{
  "party_id": 1,
  "answers": [
    {"question_id": 1, "answer": "agree"},
    {"question_id": 2, "answer": "disagree"},
    {"question_id": 3, "answer": "neutral"},
    {"question_id": 4, "answer": ""}  // Empty = unanswered
  ]
}
Example Response (200 OK)
{
  "status": "success",
  "message": "Bulk answers updated successfully",
  "data": {
    "updated_count": 4,
    "party_id": 1
  }
}
POST /party-answers/admin-bulk Super Admin Only

Update one question for all parties (super admin)

Request Body
{
  "question_id": 5,
  "answers": [
    {"party_id": 1, "answer": "agree"},
    {"party_id": 2, "answer": ""},
    {"party_id": 3, "answer": "disagree"}
  ]
}
Example Response (200 OK)
{
  "status": "success",
  "message": "Bulk answers updated successfully by admin",
  "data": {
    "updated_count": 3,
    "question_id": 5
  }
}
GET /party-answers/stats/by-category Auth Required

Get answered questions stats by category

Query Parameters
party_id (required): Party ID
Example Request
GET /api/party-answers/stats/by-category?party_id=1
Authorization: Bearer {your_token}
Example Response (200 OK)
{
  "status": "success",
  "message": "Success",
  "data": {
    "stats": [
      {
        "category_id": 1,
        "category_name": "Economy",
        "total_questions": 15,
        "answered_questions": 12,
        "unanswered_questions": 3,
        "percentage_complete": 80
      }
    ]
  }
}
Note
Only "agree" and "disagree" count as answered.
"neutral" and "" (empty) count as unanswered.

7. Party-Admin Links (3 endpoints)

GET /party-admin-links

Get party-admin relationships

POST /party-admin-links Super Admin

Link admin to party

Request Body
{
  "admin_id": 2,
  "party_id": 3
}
Example Response (201 Created)
{
  "status": "success",
  "message": "Party-admin link created successfully",
  "data": {
    "admin_id": 2,
    "party_id": 3,
    "assigned_at": "2025-11-10T15:30:00+00:00"
  }
}
DELETE /party-admin-links/{admin_id}/{party_id}

Remove party-admin link

8. Statistics (3 endpoints)

GET /stats/summary

Get dashboard summary statistics

GET /stats/daily

Get daily statistics with date range

GET /stats/party/{party_id}

Get party-specific statistics

9. File Uploads (3 endpoints)

POST /uploads/admin-profile Auth Required

Upload admin profile picture (max 5MB)

Request (multipart/form-data)
file: [image file]
admin_id: 1

Allowed formats: jpg, jpeg, png, gif
Max size: 5MB
Example cURL Request
curl -X POST http://localhost:8000/api/uploads/admin-profile \
  -H "Authorization: Bearer {token}" \
  -F "file=@/path/to/profile.jpg" \
  -F "admin_id=1"
Example Response (200 OK)
{
  "status": "success",
  "message": "Admin profile picture uploaded successfully",
  "data": {
    "file_url": "uploads/admins/admin_1_profile.jpg",
    "file_size": 1234567,
    "old_file_deleted": true
  }
}
POST /uploads/party-logo Auth Required

Upload party logo (max 5MB)

Request (multipart/form-data)
file: [image file]
party_id: 3

Allowed formats: jpg, jpeg, png, gif
Max size: 5MB
Example cURL Request
curl -X POST http://localhost:8000/api/uploads/party-logo \
  -H "Authorization: Bearer {token}" \
  -F "file=@/path/to/logo.png" \
  -F "party_id=3"
Example Response (200 OK)
{
  "status": "success",
  "message": "Party logo uploaded successfully",
  "data": {
    "file_url": "uploads/parties/party_3_logo.png",
    "file_size": 987654,
    "old_file_deleted": false
  }
}
DELETE /uploads

Delete uploaded file

10. Party Coordination (2 endpoints)

GET /party-coordination

Get party coordination data for compass

POST /party-coordination/upsert Super Admin

Update party coordination data

Request Body
{
  "party_id": 1,
  "left_points": 12,
  "right_points": 5,
  "progressive_points": 15,
  "conservative_points": 3
}
Example Response (200 OK)
{
  "status": "success",
  "message": "Party coordination updated successfully",
  "data": {
    "party_id": 1,
    "left_points": 12,
    "right_points": 5,
    "progressive_points": 15,
    "conservative_points": 3,
    "x_position": 7,
    "y_position": 12,
    "updated_at": "2025-11-10T10:00:00+00:00"
  }
}

11. Coordinate Calculator (4 endpoints)

POST /calculate/coordinates/parties Super Admin

Calculate coordinates for one or all parties

Request Body (Option 1 - Specific Parties)
{
  "party_ids": [1, 2, 3]
}
Request Body (Option 2 - All Parties)
{
  "recalculate_all": true
}
Note
Empty ("") and neutral answers are skipped.
Only agree/disagree answers contribute to coordinates.
Example Response (200 OK)
{
  "status": "success",
  "message": "Party coordinates calculated successfully",
  "data": {
    "calculated": [
      {
        "party_id": 1,
        "party_name": "Progressive Party",
        "coordinates": {
          "left_points": 12,
          "right_points": 5,
          "progressive_points": 15,
          "conservative_points": 3
        },
        "compass_position": {
          "x": -7,
          "y": 12
        }
      }
    ],
    "total_calculated": 1
  }
}
POST /calculate/coordinates/user No Auth

Calculate user position (anonymous, no storage)

Request Body
{
  "answers": [
    {"question_id": 1, "answer": "agree"},
    {"question_id": 2, "answer": "disagree"},
    {"question_id": 3, "answer": "neutral"},
    {"question_id": 4, "answer": ""}
  ]
}
Note
Empty ("") and neutral answers are skipped.
User position is calculated but NOT stored.
Example Response (200 OK)
{
  "status": "success",
  "message": "User coordinates calculated successfully",
  "data": {
    "coordinates": {
      "left_points": 8,
      "right_points": 3,
      "progressive_points": 10,
      "conservative_points": 2
    },
    "compass_position": {
      "x": -5,
      "y": 8
    },
    "total_questions_answered": 2
  }
}
GET /calculate/coordinates/parties/{party_id}

Get stored party coordinates

GET /calculate/coordinates/parties

Get all party coordinates for compass visualization

12. Match Calculator (3 endpoints)

User answers support agree, disagree, neutral, and "" (empty). Empty answers don't contribute to matching.
POST /calculate/matches No Auth

Calculate top 3 party matches (anonymous)

Request Body
{
  "answers": [
    {"question_id": 1, "answer": "agree"},
    {"question_id": 2, "answer": "disagree"},
    {"question_id": 3, "answer": "neutral"},
    {"question_id": 4, "answer": ""}  // Empty = skip
  ],
  "weighted_questions": [
    {"question_id": 1, "weight": 3}
  ]
}
Example Response (200 OK)
{
  "status": "success",
  "message": "Top matches calculated successfully",
  "data": {
    "top_matches": [
      {
        "rank": 1,
        "party_id": 1,
        "party_name": "Progressive Party",
        "match_percentage": 87.5,
        "agreement_score": 42,
        "total_questions_compared": 3,
        "match_breakdown": {
          "full_agreement": 2,
          "partial_agreement": 1,
          "disagreement": 0
        }
      }
    ],
    "total_questions": 4,
    "answered_questions": 3
  }
}
POST /calculate/matches/detailed No Auth

Calculate matches for all parties with breakdown

Request Body
{
  "answers": [
    {"question_id": 1, "answer": "agree"},
    {"question_id": 2, "answer": "disagree"}
  ],
  "weighted_questions": [
    {"question_id": 1, "weight": 2}
  ]
}
Example Response (200 OK)
{
  "status": "success",
  "message": "Detailed matches calculated successfully",
  "data": {
    "matches": [
      {
        "party_id": 1,
        "party_name": "Progressive Party",
        "match_percentage": 85.0,
        "agreement_score": 34,
        "match_breakdown": {
          "full_agreement": 8,
          "partial_agreement": 4,
          "disagreement": 2
        }
      }
    ],
    "total_parties": 12
  }
}
POST /calculate/matches/stats

Get match calculation statistics

13. Health Check (1 endpoint)

GET /health

API health check (no authentication required)

Testing & Examples

Test Script Available

Run php test_api.php to automatically test the API endpoints

Example: Register and Login
# Register a super admin
curl -X POST http://localhost:8000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "admin@stemwijzer.nl",
    "password": "admin123",
    "name": "Super Admin",
    "role": "super_admin"
  }'

# Login to get token
curl -X POST http://localhost:8000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "admin@stemwijzer.nl",
    "password": "admin123"
  }'
Example: Create Party with Auto-Admin
curl -X POST http://localhost:8000/api/parties \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "name": "Green Party",
    "description": "Environmental and social justice",
    "party_link": "https://greenparty.nl",
    "create_admin": true,
    "admin_username": "green@stemwijzer.nl",
    "admin_password": "green123"
  }'
Example: Calculate Matches (Anonymous)
curl -X POST http://localhost:8000/api/calculate/matches \
  -H "Content-Type: application/json" \
  -d '{
    "answers": [
      {"question_id": 1, "answer": "agree"},
      {"question_id": 2, "answer": "disagree"},
      {"question_id": 3, "answer": "neutral"}
    ],
    "weighted_questions": [
      {"question_id": 1, "weight": 3}
    ]
  }'
Success Response Format
{
  "status": "success",
  "message": "Operation successful",
  "data": {
    // Response data here
  }
}
Error Response Format
{
  "status": "error",
  "message": "Error description",
  "errors": {
    "field_name": ["Error detail"]
  },
  "code": 400
}

Documentation

API Specification

Complete API specification with all endpoint details, request/response formats, and examples.

View Specification
API Worksheet

Implementation status, testing examples, and quick reference guide.

View Worksheet