Complete Political Compass & Party Matching System
Last Updated: November 10, 2025
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
Secure token-based authentication with bcrypt password hashing
Super admin and party admin permissions
Automatic party answers, codes, and admin accounts
Calculate party positions on 2D compass
Find top 3 party matches for users
Profile pictures and party logos with auto-cleanup
Transaction-based bulk updates
Comprehensive analytics and reporting
Track unanswered questions with empty string state
Authenticate user and receive JWT token
{
"username": "admin@stemwijzer.nl",
"password": "admin123"
}
{
"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"
}
}
Register new admin account
{
"username": "newadmin@stemwijzer.nl",
"password": "securepassword123",
"name": "New Admin",
"role": "admin" // or "super_admin"
}
{
"status": "success",
"message": "Registration successful",
"data": {
"admin_id": 2,
"username": "newadmin@stemwijzer.nl",
"name": "New Admin",
"role": "admin"
}
}
Invalidate authentication token
Authorization: Bearer {your_token}
{
"status": "success",
"message": "Logout successful"
}
Validate current token and get expiration info
Authorization: Bearer {your_token}
{
"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"
}
}
List all admins (super admin only)
Get admin details with associated parties
Update admin profile (name, picture)
{
"name": "Updated Admin Name",
"profile_picture_url": "uploads/admins/admin_123.jpg"
}
PUT /api/admins/1
Authorization: Bearer {your_token}
Content-Type: application/json
{
"name": "John Doe Admin"
}
{
"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"
}
}
Change admin password
{
"current_password": "oldpassword123",
"new_password": "newsecurepassword456"
}
PUT /api/admins/1/password
Authorization: Bearer {your_token}
Content-Type: application/json
{
"current_password": "admin123",
"new_password": "newpassword123"
}
{
"status": "success",
"message": "Password updated successfully"
}
List all parties with pagination and sorting
Get party details with admins
Create party with auto-generation (answers, admin account)
{
"name": "New Political Party",
"short_code": "NPP",
"party_link": "https://newparty.nl",
"profile_picture_url": "uploads/parties/npp_logo.jpg"
}
✓ Creates admin account: {short_code}@stemwijzer.nl / password123
✓ Generates party answers with empty ("") for all questions
✓ Creates party-admin link automatically
{
"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"
}
}
}
Update party information
{
"name": "Updated Party Name",
"short_code": "UPN",
"party_link": "https://updatedparty.nl",
"profile_picture_url": "uploads/parties/upn_logo.jpg"
}
PUT /api/parties/1
Authorization: Bearer {your_token}
Content-Type: application/json
{
"name": "Progressive Alliance",
"party_link": "https://progressive-alliance.nl"
}
{
"status": "success",
"message": "Party updated successfully",
"data": {
"party_id": 1,
"name": "Progressive Alliance",
"short_code": "PA",
"party_link": "https://progressive-alliance.nl"
}
}
Delete party with cascade (super admin only)
List all categories with question counts
Get category details
Create new category
{
"name": "Economy",
"description": "Questions about economic policy"
}
{
"status": "success",
"message": "Category created successfully",
"data": {
"category_id": 3,
"name": "Economy",
"description": "Questions about economic policy",
"question_count": 0
}
}
Update category
{
"name": "Economic Policy",
"description": "Updated description about economic matters"
}
{
"status": "success",
"message": "Category updated successfully",
"data": {
"category_id": 3,
"name": "Economic Policy",
"description": "Updated description about economic matters"
}
}
Delete category (only if no questions)
List all questions with category names
Get question details
Create question with auto-generation (party answers, order)
{
"question_text": "Should taxes be increased?",
"category_id": 1,
"agree_direction": "Left",
"disagree_direction": "Right",
"order_index": 5
}
✓ Creates empty ("") party answers for all existing parties
✓ Auto-assigns order_index if not provided
✓ Links question to category
{
"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
}
}
Update question
{
"question_text": "Should income taxes be increased?",
"category_id": 1,
"agree_direction": "Left",
"disagree_direction": "Right",
"order_index": 3
}
{
"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 question with cascade
agree,
disagree, neutral, and "" (empty string for unanswered questions).
Get party answers with filters
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)
GET /api/party-answers?party_id=1&category_id=2
Authorization: Bearer {your_token}
{
"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
}
]
}
}
Update single party answer
{
"answer": "agree" // Can be: "agree", "disagree", "neutral", or "" (empty)
}
PUT /api/party-answers/1/5
Authorization: Bearer {your_token}
Content-Type: application/json
{
"answer": "" // Mark as unanswered
}
{
"status": "success",
"message": "Answer updated successfully",
"data": {
"party_answer": {
"party_id": 1,
"question_id": 5,
"answer": "",
"update_counter": 3
}
}
}
Bulk update party answers (transaction-based)
{
"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
]
}
{
"status": "success",
"message": "Bulk answers updated successfully",
"data": {
"updated_count": 4,
"party_id": 1
}
}
Update one question for all parties (super admin)
{
"question_id": 5,
"answers": [
{"party_id": 1, "answer": "agree"},
{"party_id": 2, "answer": ""},
{"party_id": 3, "answer": "disagree"}
]
}
{
"status": "success",
"message": "Bulk answers updated successfully by admin",
"data": {
"updated_count": 3,
"question_id": 5
}
}
Get answered questions stats by category
party_id (required): Party ID
GET /api/party-answers/stats/by-category?party_id=1
Authorization: Bearer {your_token}
{
"status": "success",
"message": "Success",
"data": {
"stats": [
{
"category_id": 1,
"category_name": "Economy",
"total_questions": 15,
"answered_questions": 12,
"unanswered_questions": 3,
"percentage_complete": 80
}
]
}
}
Only "agree" and "disagree" count as answered. "neutral" and "" (empty) count as unanswered.
Get party-admin relationships
Link admin to party
{
"admin_id": 2,
"party_id": 3
}
{
"status": "success",
"message": "Party-admin link created successfully",
"data": {
"admin_id": 2,
"party_id": 3,
"assigned_at": "2025-11-10T15:30:00+00:00"
}
}
Remove party-admin link
Get dashboard summary statistics
Get daily statistics with date range
Get party-specific statistics
Upload admin profile picture (max 5MB)
file: [image file] admin_id: 1 Allowed formats: jpg, jpeg, png, gif Max size: 5MB
curl -X POST http://localhost:8000/api/uploads/admin-profile \
-H "Authorization: Bearer {token}" \
-F "file=@/path/to/profile.jpg" \
-F "admin_id=1"
{
"status": "success",
"message": "Admin profile picture uploaded successfully",
"data": {
"file_url": "uploads/admins/admin_1_profile.jpg",
"file_size": 1234567,
"old_file_deleted": true
}
}
Upload party logo (max 5MB)
file: [image file] party_id: 3 Allowed formats: jpg, jpeg, png, gif Max size: 5MB
curl -X POST http://localhost:8000/api/uploads/party-logo \
-H "Authorization: Bearer {token}" \
-F "file=@/path/to/logo.png" \
-F "party_id=3"
{
"status": "success",
"message": "Party logo uploaded successfully",
"data": {
"file_url": "uploads/parties/party_3_logo.png",
"file_size": 987654,
"old_file_deleted": false
}
}
Delete uploaded file
Get party coordination data for compass
Update party coordination data
{
"party_id": 1,
"left_points": 12,
"right_points": 5,
"progressive_points": 15,
"conservative_points": 3
}
{
"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"
}
}
Calculate coordinates for one or all parties
{
"party_ids": [1, 2, 3]
}
{
"recalculate_all": true
}
Empty ("") and neutral answers are skipped.
Only agree/disagree answers contribute to coordinates.
{
"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
}
}
Calculate user position (anonymous, no storage)
{
"answers": [
{"question_id": 1, "answer": "agree"},
{"question_id": 2, "answer": "disagree"},
{"question_id": 3, "answer": "neutral"},
{"question_id": 4, "answer": ""}
]
}
Empty ("") and neutral answers are skipped.
User position is calculated but NOT stored.
{
"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 stored party coordinates
Get all party coordinates for compass visualization
agree, disagree,
neutral, and "" (empty). Empty answers don't contribute to matching.
Calculate top 3 party matches (anonymous)
{
"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}
]
}
{
"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
}
}
Calculate matches for all parties with breakdown
{
"answers": [
{"question_id": 1, "answer": "agree"},
{"question_id": 2, "answer": "disagree"}
],
"weighted_questions": [
{"question_id": 1, "weight": 2}
]
}
{
"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
}
}
Get match calculation statistics
API health check (no authentication required)
Run php test_api.php to automatically test the API endpoints
# 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"
}'
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"
}'
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}
]
}'
{
"status": "success",
"message": "Operation successful",
"data": {
// Response data here
}
}
{
"status": "error",
"message": "Error description",
"errors": {
"field_name": ["Error detail"]
},
"code": 400
}
Complete API specification with all endpoint details, request/response formats, and examples.
View Specification