Stakeholder Engagement API
Status: Final Version: 1.0 Last Updated: 2026-01-17
Purpose
Define REST endpoints for managing stakeholder engagement records and grievance mechanisms to support GRI 2-29 (Approach to stakeholder engagement) and related stakeholder reporting requirements.
This API enables: - Collectors: Submit stakeholder engagement logs and grievance records - Reviewers/Approvers: Review and validate stakeholder engagement data - Admins: Manage stakeholder categories, engagement platforms, and export reports - All Roles: Track engagement activities and grievance resolution status
Table of Contents
- Authentication
- Master Data Management
- Stakeholder Engagement Records
- Grievance Records
- Evidence Management
- Report Export
- Filtering and Pagination
- Error Handling
- PII Protection
- Implementation Guide
Authentication
All stakeholder engagement endpoints require JWT authentication. See API Overview - Authentication for details.
Quick Example:
GET /api/v1/stakeholder-engagement/categories
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Master Data Management
GET /api/v1/stakeholder-engagement/categories
Fetch stakeholder category master list (e.g., Community, Employees, Suppliers, Investors).
Authentication: Required (COLLECTOR, REVIEWER, APPROVER, ADMIN roles)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
includeInactive |
boolean | No | Include deactivated categories (default: false) |
Request Example:
Response (200 OK):
{
"data": [
{
"id": "cat-uuid-1",
"name": "Community",
"description": "Local communities affected by operations",
"isActive": true,
"createdAt": "2025-01-01T10:00:00Z",
"updatedAt": "2025-01-01T10:00:00Z"
},
{
"id": "cat-uuid-2",
"name": "Employees",
"description": "Full-time and contract employees",
"isActive": true,
"createdAt": "2025-01-01T10:00:00Z",
"updatedAt": "2025-01-01T10:00:00Z"
},
{
"id": "cat-uuid-3",
"name": "Suppliers",
"description": "Supply chain partners and vendors",
"isActive": true,
"createdAt": "2025-01-01T10:00:00Z",
"updatedAt": "2025-01-01T10:00:00Z"
},
{
"id": "cat-uuid-4",
"name": "Investors",
"description": "Shareholders and financial stakeholders",
"isActive": true,
"createdAt": "2025-01-01T10:00:00Z",
"updatedAt": "2025-01-01T10:00:00Z"
}
],
"pagination": {
"totalItems": 4
}
}
Error Responses:
| Status | Error Code | Description |
|---|---|---|
| 401 | AUTH_TOKEN_INVALID | Invalid or expired JWT token |
| 403 | AUTH_INSUFFICIENT_PERMISSIONS | User lacks required role |
POST /api/v1/stakeholder-engagement/categories
Create a new stakeholder category.
Authentication: Required (ADMIN role)
Request Body:
Response (201 Created):
{
"id": "cat-uuid-5",
"name": "Regulators",
"description": "Government regulatory bodies and agencies",
"isActive": true,
"createdAt": "2026-01-17T10:00:00Z",
"updatedAt": "2026-01-17T10:00:00Z"
}
Error Responses:
| Status | Error Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid request format or missing required fields |
| 401 | AUTH_TOKEN_INVALID | Invalid or expired JWT token |
| 403 | AUTH_INSUFFICIENT_PERMISSIONS | User lacks ADMIN role |
| 409 | RESOURCE_ALREADY_EXISTS | Category with this name already exists |
PATCH /api/v1/stakeholder-engagement/categories/{id}
Update or deactivate a stakeholder category.
Authentication: Required (ADMIN role)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Category UUID |
Request Body:
{
"name": "Local Communities",
"description": "Communities within 10km of operations",
"isActive": true
}
Response (200 OK):
{
"id": "cat-uuid-1",
"name": "Local Communities",
"description": "Communities within 10km of operations",
"isActive": true,
"createdAt": "2025-01-01T10:00:00Z",
"updatedAt": "2026-01-17T10:15:00Z"
}
GET /api/v1/stakeholder-engagement/platforms
Fetch engagement platform master list (e.g., Community Meeting, Written Correspondence, Online Survey).
Authentication: Required (COLLECTOR, REVIEWER, APPROVER, ADMIN roles)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
includeInactive |
boolean | No | Include deactivated platforms (default: false) |
Request Example:
Response (200 OK):
{
"data": [
{
"id": "plat-uuid-1",
"name": "Community Meeting",
"description": "In-person community forums and town halls",
"isActive": true,
"createdAt": "2025-01-01T10:00:00Z",
"updatedAt": "2025-01-01T10:00:00Z"
},
{
"id": "plat-uuid-2",
"name": "Written Correspondence",
"description": "Letters, emails, and formal written communication",
"isActive": true,
"createdAt": "2025-01-01T10:00:00Z",
"updatedAt": "2025-01-01T10:00:00Z"
},
{
"id": "plat-uuid-3",
"name": "Online Survey",
"description": "Digital surveys and feedback forms",
"isActive": true,
"createdAt": "2025-01-01T10:00:00Z",
"updatedAt": "2025-01-01T10:00:00Z"
}
],
"pagination": {
"totalItems": 3
}
}
POST /api/v1/stakeholder-engagement/platforms
Create a new engagement platform.
Authentication: Required (ADMIN role)
Request Body:
{
"name": "Social Media Engagement",
"description": "Engagement via Twitter, LinkedIn, and Facebook"
}
Response (201 Created):
{
"id": "plat-uuid-4",
"name": "Social Media Engagement",
"description": "Engagement via Twitter, LinkedIn, and Facebook",
"isActive": true,
"createdAt": "2026-01-17T10:00:00Z",
"updatedAt": "2026-01-17T10:00:00Z"
}
PATCH /api/v1/stakeholder-engagement/platforms/{id}
Update or deactivate an engagement platform.
Authentication: Required (ADMIN role)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Platform UUID |
Request Body:
{
"name": "Public Consultation",
"description": "Formal public consultation sessions",
"isActive": false
}
Response (200 OK):
{
"id": "plat-uuid-1",
"name": "Public Consultation",
"description": "Formal public consultation sessions",
"isActive": false,
"createdAt": "2025-01-01T10:00:00Z",
"updatedAt": "2026-01-17T10:20:00Z"
}
Stakeholder Engagement Records
POST /api/v1/stakeholder-engagement/engagements
Create a new stakeholder engagement record. Supports idempotency to prevent duplicate submissions.
Authentication: Required (COLLECTOR role)
Headers:
| Header | Required | Description |
|---|---|---|
Authorization |
Yes | Bearer JWT token |
Idempotency-Key |
Yes | UUID v4 for idempotent submission |
Content-Type |
Yes | application/json |
Request Body:
{
"engagementUuid": "eng-550e8400-e29b-41d4-a716-446655440000",
"reportingPeriodId": "period-uuid",
"organisationId": "org-uuid",
"siteId": "site-uuid",
"initialDate": "2026-01-15",
"stakeholderCategories": ["cat-uuid-1", "cat-uuid-2"],
"stakeholderName": "Local Community Leaders",
"engagementPurpose": "Discuss environmental impact mitigation plans",
"engagementPlatforms": ["plat-uuid-1", "plat-uuid-2"],
"outcome": "Agreed on quarterly monitoring meetings",
"responsiblePerson": "Jane Smith",
"status": "Closed",
"statusDate": "2026-01-15"
}
Field Descriptions:
| Field | Type | Required | Description |
|---|---|---|---|
engagementUuid |
UUID | Yes | Client-generated UUID for deduplication |
reportingPeriodId |
UUID | Yes | Target reporting period |
organisationId |
UUID | Yes | Organisation responsible for engagement |
siteId |
UUID | No | Site where engagement occurred (if applicable) |
initialDate |
Date | Yes | Date when engagement began (ISO 8601) |
stakeholderCategories |
Array[UUID] | Yes | Stakeholder category IDs (1 or more) |
stakeholderName |
String | Yes | Name of stakeholder group/organization (max 255 chars) |
engagementPurpose |
String | Yes | Purpose of engagement (max 1000 chars) |
engagementPlatforms |
Array[UUID] | Yes | Engagement platform IDs (1 or more) |
outcome |
String | No | Outcome of engagement (required if status=Closed, max 1000 chars) |
responsiblePerson |
String | Yes | Person responsible for engagement (max 255 chars) |
status |
String | Yes | Engagement status: Open, WIP, Closed |
statusDate |
Date | No | Date when status was last updated |
Response (201 Created):
{
"id": "engagement-uuid",
"engagementUuid": "eng-550e8400-e29b-41d4-a716-446655440000",
"reportingPeriod": {
"id": "period-uuid",
"name": "FY2026",
"startDate": "2026-01-01",
"endDate": "2026-12-31"
},
"organisation": {
"id": "org-uuid",
"name": "Acme Corporation"
},
"site": {
"id": "site-uuid",
"name": "Factory A",
"code": "FAC-A"
},
"initialDate": "2026-01-15",
"stakeholderCategories": [
{
"id": "cat-uuid-1",
"name": "Community"
},
{
"id": "cat-uuid-2",
"name": "Employees"
}
],
"stakeholderName": "Local Community Leaders",
"engagementPurpose": "Discuss environmental impact mitigation plans",
"engagementPlatforms": [
{
"id": "plat-uuid-1",
"name": "Community Meeting"
},
{
"id": "plat-uuid-2",
"name": "Written Correspondence"
}
],
"outcome": "Agreed on quarterly monitoring meetings",
"responsiblePerson": "Jane Smith",
"status": "Closed",
"statusDate": "2026-01-15",
"createdAt": "2026-01-17T10:30:00Z",
"updatedAt": "2026-01-17T10:30:00Z",
"createdBy": {
"id": "user-uuid",
"name": "John Collector",
"email": "john@example.com"
},
"evidenceCount": 0,
"nextSteps": [
"Upload evidence files (meeting minutes, attendance registers)",
"Evidence is optional but recommended for audit trail"
]
}
Response (200 OK - Idempotent Retry):
If the same Idempotency-Key is used within 24 hours, the original 201 response is returned:
{
"id": "engagement-uuid",
"engagementUuid": "eng-550e8400-e29b-41d4-a716-446655440000",
"status": "Closed",
"createdAt": "2026-01-17T10:30:00Z",
"message": "This engagement was already created (idempotent retry)"
}
Error Responses:
| Status | Error Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid request format or missing required fields |
| 401 | AUTH_TOKEN_INVALID | Invalid or expired JWT token |
| 403 | AUTH_SCOPE_VIOLATION | User cannot submit to this organisation/site |
| 404 | RESOURCE_NOT_FOUND | Reporting period, category, or platform not found |
| 409 | RESOURCE_ALREADY_EXISTS | Duplicate engagementUuid (idempotency bypass) |
| 409 | RESOURCE_LOCKED | Reporting period is locked |
| 422 | VALIDATION_RULE_FAILED | Business rule violation |
Error Example (422 - Validation Failed):
{
"error": "VALIDATION_RULE_FAILED",
"message": "Engagement submission failed validation",
"timestamp": "2026-01-17T10:30:00Z",
"request_id": "req_abc123",
"details": [
{
"field": "outcome",
"code": "FIELD_REQUIRED_WHEN_CLOSED",
"message": "Outcome is required when status is Closed"
},
{
"field": "stakeholderCategories",
"code": "ARRAY_MIN_LENGTH",
"message": "At least one stakeholder category is required"
},
{
"field": "initialDate",
"code": "DATE_OUT_OF_RANGE",
"message": "Initial date must be within reporting period 2026-01-01 to 2026-12-31"
}
]
}
GET /api/v1/stakeholder-engagement/engagements
Retrieve stakeholder engagement records with filtering and pagination.
Authentication: Required (COLLECTOR, REVIEWER, APPROVER, ADMIN roles)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
reportingPeriodId |
UUID | No | Filter by reporting period |
organisationId |
UUID | No | Filter by organisation |
siteId |
UUID | No | Filter by site |
status |
String | No | Filter by status (Open, WIP, Closed) |
stakeholderCategoryId |
UUID | No | Filter by stakeholder category |
engagementPlatformId |
UUID | No | Filter by engagement platform |
initialDateFrom |
ISO 8601 | No | Filter engagements from this date |
initialDateTo |
ISO 8601 | No | Filter engagements until this date |
search |
String | No | Full-text search in purpose, outcome, and stakeholder name |
page |
Integer | No | Page number (default: 1) |
pageSize |
Integer | No | Items per page (default: 50, max: 100) |
sort |
String | No | Sort field and direction (e.g., initialDate:desc) |
Request Example:
GET /api/v1/stakeholder-engagement/engagements?reportingPeriodId=period-uuid&status=Open&page=1&pageSize=20
Authorization: Bearer token...
Response (200 OK):
{
"data": [
{
"id": "engagement-uuid-1",
"engagementUuid": "eng-550e8400-e29b-41d4-a716-446655440000",
"reportingPeriod": {
"id": "period-uuid",
"name": "FY2026"
},
"organisation": {
"id": "org-uuid",
"name": "Acme Corporation"
},
"site": {
"id": "site-uuid",
"name": "Factory A"
},
"initialDate": "2026-01-15",
"stakeholderCategories": [
{
"id": "cat-uuid-1",
"name": "Community"
}
],
"stakeholderName": "Local Community Leaders",
"engagementPurpose": "Discuss environmental impact mitigation plans",
"engagementPlatforms": [
{
"id": "plat-uuid-1",
"name": "Community Meeting"
}
],
"outcome": null,
"responsiblePerson": "Jane Smith",
"status": "Open",
"statusDate": "2026-01-15",
"evidenceCount": 0,
"createdAt": "2026-01-17T10:30:00Z",
"updatedAt": "2026-01-17T10:30:00Z"
}
],
"pagination": {
"page": 1,
"pageSize": 20,
"totalPages": 3,
"totalItems": 47,
"hasNext": true,
"hasPrevious": false
},
"links": {
"self": "/api/v1/stakeholder-engagement/engagements?reportingPeriodId=period-uuid&status=Open&page=1&pageSize=20",
"next": "/api/v1/stakeholder-engagement/engagements?reportingPeriodId=period-uuid&status=Open&page=2&pageSize=20",
"last": "/api/v1/stakeholder-engagement/engagements?reportingPeriodId=period-uuid&status=Open&page=3&pageSize=20"
}
}
GET /api/v1/stakeholder-engagement/engagements/{id}
Retrieve detailed information about a specific engagement.
Authentication: Required (COLLECTOR, REVIEWER, APPROVER, ADMIN roles)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Engagement UUID |
Request Example:
Response (200 OK):
{
"id": "engagement-uuid",
"engagementUuid": "eng-550e8400-e29b-41d4-a716-446655440000",
"reportingPeriod": {
"id": "period-uuid",
"name": "FY2026",
"startDate": "2026-01-01",
"endDate": "2026-12-31"
},
"organisation": {
"id": "org-uuid",
"name": "Acme Corporation"
},
"site": {
"id": "site-uuid",
"name": "Factory A",
"code": "FAC-A"
},
"initialDate": "2026-01-15",
"stakeholderCategories": [
{
"id": "cat-uuid-1",
"name": "Community"
},
{
"id": "cat-uuid-2",
"name": "Employees"
}
],
"stakeholderName": "Local Community Leaders",
"engagementPurpose": "Discuss environmental impact mitigation plans",
"engagementPlatforms": [
{
"id": "plat-uuid-1",
"name": "Community Meeting"
},
{
"id": "plat-uuid-2",
"name": "Written Correspondence"
}
],
"outcome": "Agreed on quarterly monitoring meetings",
"responsiblePerson": "Jane Smith",
"status": "Closed",
"statusDate": "2026-01-15",
"evidence": [
{
"id": "evidence-uuid",
"filename": "meeting_minutes_jan2026.pdf",
"evidenceType": "MEETING_MINUTES",
"fileSize": 128540,
"uploadedAt": "2026-01-17T10:35:00Z"
}
],
"createdAt": "2026-01-17T10:30:00Z",
"updatedAt": "2026-01-17T10:30:00Z",
"createdBy": {
"id": "user-uuid",
"name": "John Collector",
"email": "john@example.com"
}
}
Error Responses:
| Status | Error Code | Description |
|---|---|---|
| 401 | AUTH_TOKEN_INVALID | Invalid or expired JWT token |
| 403 | AUTH_SCOPE_VIOLATION | User cannot access this engagement |
| 404 | RESOURCE_NOT_FOUND | Engagement not found |
PATCH /api/v1/stakeholder-engagement/engagements/{id}
Update an existing stakeholder engagement record. Engagement records can be updated at any time (no DRAFT state required).
Authentication: Required (COLLECTOR, REVIEWER, ADMIN roles)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Engagement UUID |
Request Body:
{
"status": "Closed",
"statusDate": "2026-01-20",
"outcome": "Agreement reached on mitigation measures"
}
Response (200 OK):
{
"id": "engagement-uuid",
"engagementUuid": "eng-550e8400-e29b-41d4-a716-446655440000",
"status": "Closed",
"statusDate": "2026-01-20",
"outcome": "Agreement reached on mitigation measures",
"updatedAt": "2026-01-20T14:00:00Z",
"message": "Engagement updated successfully"
}
Error Responses:
| Status | Error Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid request format |
| 401 | AUTH_TOKEN_INVALID | Invalid or expired JWT token |
| 403 | AUTH_SCOPE_VIOLATION | User cannot modify this engagement |
| 404 | RESOURCE_NOT_FOUND | Engagement not found |
| 409 | RESOURCE_LOCKED | Reporting period is locked |
| 422 | VALIDATION_RULE_FAILED | Updated value fails validation |
Grievance Records
POST /api/v1/stakeholder-engagement/grievances
Create a new grievance record. Supports idempotency to prevent duplicate submissions.
Authentication: Required (COLLECTOR role)
Headers:
| Header | Required | Description |
|---|---|---|
Authorization |
Yes | Bearer JWT token |
Idempotency-Key |
Yes | UUID v4 for idempotent submission |
Content-Type |
Yes | application/json |
Request Body:
{
"grievanceUuid": "grv-660e8400-e29b-41d4-a716-446655440000",
"reportingPeriodId": "period-uuid",
"organisationId": "org-uuid",
"siteId": "site-uuid",
"dateReported": "2026-01-10",
"isInternal": false,
"stakeholderGroup": "Local community members",
"natureOfGrievance": "Concerns about air quality from factory emissions",
"intervention": "Environmental assessment commissioned; community meeting scheduled",
"resolutionStatus": "WIP"
}
Field Descriptions:
| Field | Type | Required | Description |
|---|---|---|---|
grievanceUuid |
UUID | Yes | Client-generated UUID for deduplication |
reportingPeriodId |
UUID | Yes | Target reporting period |
organisationId |
UUID | Yes | Organisation where grievance was raised |
siteId |
UUID | No | Site where grievance occurred (if applicable) |
dateReported |
Date | Yes | Date grievance was reported (ISO 8601) |
isInternal |
Boolean | Yes | Internal (employee) or external (community/supplier) |
stakeholderGroup |
String | Yes | General stakeholder group (NOT individual names, max 255 chars) |
natureOfGrievance |
String | Yes | Description of grievance (max 1000 chars) |
intervention |
String | No | Action taken to address grievance (max 1000 chars) |
resolutionStatus |
String | Yes | Resolution status: Open, WIP, Closed |
Response (201 Created):
{
"id": "grievance-uuid",
"grievanceUuid": "grv-660e8400-e29b-41d4-a716-446655440000",
"reportingPeriod": {
"id": "period-uuid",
"name": "FY2026",
"startDate": "2026-01-01",
"endDate": "2026-12-31"
},
"organisation": {
"id": "org-uuid",
"name": "Acme Corporation"
},
"site": {
"id": "site-uuid",
"name": "Factory A",
"code": "FAC-A"
},
"dateReported": "2026-01-10",
"isInternal": false,
"stakeholderGroup": "Local community members",
"natureOfGrievance": "Concerns about air quality from factory emissions",
"intervention": "Environmental assessment commissioned; community meeting scheduled",
"resolutionStatus": "WIP",
"createdAt": "2026-01-17T11:00:00Z",
"updatedAt": "2026-01-17T11:00:00Z",
"createdBy": {
"id": "user-uuid",
"name": "John Collector",
"email": "john@example.com"
},
"evidenceCount": 0,
"nextSteps": [
"Upload evidence files (grievance forms, correspondence)",
"Evidence is optional but recommended for audit trail"
]
}
Error Responses:
| Status | Error Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid request format or missing required fields |
| 401 | AUTH_TOKEN_INVALID | Invalid or expired JWT token |
| 403 | AUTH_SCOPE_VIOLATION | User cannot submit to this organisation/site |
| 404 | RESOURCE_NOT_FOUND | Reporting period not found |
| 409 | RESOURCE_ALREADY_EXISTS | Duplicate grievanceUuid (idempotency bypass) |
| 409 | RESOURCE_LOCKED | Reporting period is locked |
| 422 | VALIDATION_PII_DETECTED | stakeholderGroup contains individual names (PII violation) |
Error Example (422 - PII Detected):
{
"error": "VALIDATION_PII_DETECTED",
"message": "Grievance submission contains potential PII",
"timestamp": "2026-01-17T11:00:00Z",
"request_id": "req_def456",
"details": [
{
"field": "stakeholderGroup",
"code": "PII_INDIVIDUAL_NAME_DETECTED",
"message": "Use general group labels (e.g., 'Local community members') instead of individual names to protect privacy"
}
]
}
GET /api/v1/stakeholder-engagement/grievances
Retrieve grievance records with filtering and pagination.
Authentication: Required (COLLECTOR, REVIEWER, APPROVER, ADMIN roles)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
reportingPeriodId |
UUID | No | Filter by reporting period |
organisationId |
UUID | No | Filter by organisation |
siteId |
UUID | No | Filter by site |
resolutionStatus |
String | No | Filter by status (Open, WIP, Closed) |
isInternal |
Boolean | No | Filter by internal/external |
dateReportedFrom |
ISO 8601 | No | Filter grievances from this date |
dateReportedTo |
ISO 8601 | No | Filter grievances until this date |
search |
String | No | Full-text search in nature of grievance and intervention |
page |
Integer | No | Page number (default: 1) |
pageSize |
Integer | No | Items per page (default: 50, max: 100) |
sort |
String | No | Sort field and direction (e.g., dateReported:desc) |
Request Example:
GET /api/v1/stakeholder-engagement/grievances?reportingPeriodId=period-uuid&resolutionStatus=Open&page=1&pageSize=20
Authorization: Bearer token...
Response (200 OK):
{
"data": [
{
"id": "grievance-uuid-1",
"grievanceUuid": "grv-660e8400-e29b-41d4-a716-446655440000",
"reportingPeriod": {
"id": "period-uuid",
"name": "FY2026"
},
"organisation": {
"id": "org-uuid",
"name": "Acme Corporation"
},
"site": {
"id": "site-uuid",
"name": "Factory A"
},
"dateReported": "2026-01-10",
"isInternal": false,
"stakeholderGroup": "Local community members",
"natureOfGrievance": "Concerns about air quality from factory emissions",
"intervention": "Environmental assessment commissioned; community meeting scheduled",
"resolutionStatus": "WIP",
"evidenceCount": 1,
"createdAt": "2026-01-17T11:00:00Z",
"updatedAt": "2026-01-17T11:00:00Z"
}
],
"pagination": {
"page": 1,
"pageSize": 20,
"totalPages": 2,
"totalItems": 23,
"hasNext": true,
"hasPrevious": false
},
"links": {
"self": "/api/v1/stakeholder-engagement/grievances?reportingPeriodId=period-uuid&resolutionStatus=Open&page=1&pageSize=20",
"next": "/api/v1/stakeholder-engagement/grievances?reportingPeriodId=period-uuid&resolutionStatus=Open&page=2&pageSize=20",
"last": "/api/v1/stakeholder-engagement/grievances?reportingPeriodId=period-uuid&resolutionStatus=Open&page=2&pageSize=20"
}
}
GET /api/v1/stakeholder-engagement/grievances/{id}
Retrieve detailed information about a specific grievance.
Authentication: Required (COLLECTOR, REVIEWER, APPROVER, ADMIN roles)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Grievance UUID |
Request Example:
Response (200 OK):
{
"id": "grievance-uuid",
"grievanceUuid": "grv-660e8400-e29b-41d4-a716-446655440000",
"reportingPeriod": {
"id": "period-uuid",
"name": "FY2026",
"startDate": "2026-01-01",
"endDate": "2026-12-31"
},
"organisation": {
"id": "org-uuid",
"name": "Acme Corporation"
},
"site": {
"id": "site-uuid",
"name": "Factory A",
"code": "FAC-A"
},
"dateReported": "2026-01-10",
"isInternal": false,
"stakeholderGroup": "Local community members",
"natureOfGrievance": "Concerns about air quality from factory emissions",
"intervention": "Environmental assessment commissioned; community meeting scheduled",
"resolutionStatus": "WIP",
"evidence": [
{
"id": "evidence-uuid",
"filename": "grievance_form_jan2026.pdf",
"evidenceType": "GRIEVANCE_FORM",
"fileSize": 95240,
"uploadedAt": "2026-01-17T11:05:00Z"
}
],
"createdAt": "2026-01-17T11:00:00Z",
"updatedAt": "2026-01-17T11:00:00Z",
"createdBy": {
"id": "user-uuid",
"name": "John Collector",
"email": "john@example.com"
}
}
PATCH /api/v1/stakeholder-engagement/grievances/{id}
Update an existing grievance record (e.g., update intervention or resolution status).
Authentication: Required (COLLECTOR, REVIEWER, ADMIN roles)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Grievance UUID |
Request Body:
{
"intervention": "Environmental assessment completed; mitigation measures implemented",
"resolutionStatus": "Closed"
}
Response (200 OK):
{
"id": "grievance-uuid",
"grievanceUuid": "grv-660e8400-e29b-41d4-a716-446655440000",
"intervention": "Environmental assessment completed; mitigation measures implemented",
"resolutionStatus": "Closed",
"updatedAt": "2026-01-25T10:00:00Z",
"message": "Grievance updated successfully"
}
Evidence Management
POST /api/v1/stakeholder-engagement/engagements/{id}/evidence
Link uploaded evidence to an engagement. Evidence must be uploaded first via POST /api/v1/collector/evidence (see Collector API).
Authentication: Required (COLLECTOR role)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Engagement UUID |
Request Body:
Response (200 OK):
{
"engagementId": "engagement-uuid",
"evidenceId": "evidence-uuid",
"linkedAt": "2026-01-17T10:40:00Z",
"message": "Evidence linked successfully"
}
Error Responses:
| Status | Error Code | Description |
|---|---|---|
| 403 | AUTH_SCOPE_VIOLATION | User cannot modify this engagement |
| 404 | RESOURCE_NOT_FOUND | Engagement or evidence not found |
| 409 | RESOURCE_ALREADY_EXISTS | Evidence already linked to this engagement |
POST /api/v1/stakeholder-engagement/grievances/{id}/evidence
Link uploaded evidence to a grievance. Evidence must be uploaded first via POST /api/v1/collector/evidence.
Authentication: Required (COLLECTOR role)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Grievance UUID |
Request Body:
Response (200 OK):
{
"grievanceId": "grievance-uuid",
"evidenceId": "evidence-uuid",
"linkedAt": "2026-01-17T11:10:00Z",
"message": "Evidence linked successfully"
}
Report Export
POST /api/v1/stakeholder-engagement/reports/export
Generate and download stakeholder engagement report in CSV or XLSX format. Includes GRI 2-29 engagement logs and grievance mechanism data.
Authentication: Required (REVIEWER, APPROVER, ADMIN roles)
Request Body:
{
"reportingPeriodId": "period-uuid",
"organisationId": "org-uuid",
"siteId": "site-uuid",
"format": "XLSX",
"includeEngagements": true,
"includeGrievances": true,
"filterStatus": ["Open", "WIP", "Closed"]
}
Field Descriptions:
| Field | Type | Required | Description |
|---|---|---|---|
reportingPeriodId |
UUID | Yes | Reporting period to export |
organisationId |
UUID | No | Filter by organisation (default: all accessible) |
siteId |
UUID | No | Filter by site (default: all accessible) |
format |
String | Yes | Export format: CSV or XLSX |
includeEngagements |
Boolean | Yes | Include engagement log |
includeGrievances |
Boolean | Yes | Include grievance log |
filterStatus |
Array[String] | No | Filter by status (default: all statuses) |
Response (202 Accepted):
{
"jobId": "export-job-uuid",
"status": "QUEUED",
"message": "Export job queued for processing",
"estimatedCompletionTime": "2-5 minutes",
"statusUrl": "/api/v1/stakeholder-engagement/reports/export/export-job-uuid"
}
Check Export Status:
Response (200 OK - In Progress):
{
"jobId": "export-job-uuid",
"status": "PROCESSING",
"progress": 45,
"message": "Generating XLSX report"
}
Response (200 OK - Completed):
{
"jobId": "export-job-uuid",
"status": "COMPLETED",
"downloadUrl": "/api/v1/stakeholder-engagement/reports/export/export-job-uuid/download",
"expiresAt": "2026-01-18T11:00:00Z",
"fileSize": 458720,
"recordCounts": {
"engagements": 47,
"grievances": 23
}
}
Download Export:
Response (200 OK):
Returns the file binary data with appropriate headers:
HTTP/1.1 200 OK
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Content-Disposition: attachment; filename="stakeholder_engagement_FY2026.xlsx"
Content-Length: 458720
[binary file data]
XLSX Structure:
The XLSX export contains multiple sheets: 1. Categories & Platforms: Master lists of stakeholder categories and engagement platforms 2. Engagements: Complete stakeholder engagement log 3. Grievances: Complete grievance mechanism log
CSV Structure:
The CSV export generates separate files in a ZIP archive:
- stakeholder_categories.csv
- engagement_platforms.csv
- stakeholder_engagements.csv
- grievance_log.csv
Filtering and Pagination
All collection endpoints support filtering via query parameters and pagination. See API Overview - Pagination for general conventions.
Stakeholder Engagement Specific Filters:
| Filter | Applies To | Description |
|---|---|---|
reportingPeriodId |
Engagements, Grievances | Filter by reporting period |
organisationId |
Engagements, Grievances | Filter by organisation |
siteId |
Engagements, Grievances | Filter by site |
status |
Engagements | Filter engagement status (Open, WIP, Closed) |
resolutionStatus |
Grievances | Filter grievance status (Open, WIP, Closed) |
stakeholderCategoryId |
Engagements | Filter by stakeholder category |
engagementPlatformId |
Engagements | Filter by engagement platform |
isInternal |
Grievances | Filter internal/external grievances |
initialDateFrom/To |
Engagements | Filter by engagement date range |
dateReportedFrom/To |
Grievances | Filter by reporting date range |
Error Handling
All error responses follow the standard error format defined in Error Handling.
Stakeholder Engagement Specific Errors
PII Protection Error:
{
"error": "VALIDATION_PII_DETECTED",
"message": "Submission contains potential PII",
"timestamp": "2026-01-17T11:00:00Z",
"request_id": "req_ghi789",
"details": [
{
"field": "stakeholderGroup",
"code": "PII_INDIVIDUAL_NAME_DETECTED",
"message": "Use general group labels instead of individual names"
}
]
}
Business Rule Violation:
{
"error": "VALIDATION_RULE_FAILED",
"message": "Engagement validation failed",
"timestamp": "2026-01-17T10:30:00Z",
"request_id": "req_jkl012",
"details": [
{
"field": "outcome",
"code": "FIELD_REQUIRED_WHEN_CLOSED",
"message": "Outcome is required when status is Closed"
}
]
}
PII Protection
PII Handling Requirements
Stakeholder Name (Engagements): - Use organization/group names (e.g., "Local Community Leaders", "Employee Union Representatives") - Avoid individual names where possible - If individuals must be named, ensure proper consent and data protection compliance
Stakeholder Group (Grievances):
- MUST use general group labels (e.g., "Local community members", "Factory employees")
- NEVER use individual names
- PII validation enforced at API level with VALIDATION_PII_DETECTED error
Pattern Detection:
The API performs basic PII detection on the stakeholderGroup field in grievances:
- Rejects common name patterns (e.g., "John Smith", "Mr. Ahmed")
- Suggests using general group labels instead
Example Acceptable Values: - ✅ "Local community members" - ✅ "Factory shift workers" - ✅ "Supplier representatives" - ✅ "Anonymous complainant"
Example Rejected Values: - ❌ "John Smith" - ❌ "Ms. Jane Doe" - ❌ "Ahmed Al-Rashid"
GDPR/Privacy Compliance:
- Individual names in grievances violate privacy principles
- Use role-based or group-based identification
- Evidence files may contain PII but are access-controlled via RBAC
- Audit logs track all access to grievance records
Implementation Guide
Backend Implementation Notes
Quarkus JAX-RS Resource Example:
import jakarta.inject.Inject
import jakarta.validation.Valid
import jakarta.ws.rs.*
import jakarta.ws.rs.core.MediaType
import jakarta.ws.rs.core.Response
import io.quarkus.security.identity.SecurityIdentity
import java.util.UUID
@Path("/api/v1/stakeholder-engagement")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
class StakeholderEngagementResource @Inject constructor(
private val categoryService: StakeholderCategoryService,
private val engagementService: StakeholderEngagementService,
private val grievanceService: GrievanceService,
private val validationService: ValidationService,
private val securityIdentity: SecurityIdentity
) {
@GET
@Path("/categories")
fun getCategories(
@QueryParam("includeInactive") includeInactive: Boolean = false
): Response {
val user = securityIdentity.principal as AuthUser
val categories = categoryService.findAll(
tenantId = user.tenantId,
includeInactive = includeInactive
)
return Response.ok(categories.toResponse()).build()
}
@POST
@Path("/engagements")
@RolesAllowed("COLLECTOR")
fun createEngagement(
@HeaderParam("Idempotency-Key") idempotencyKey: String,
@Valid request: CreateEngagementRequest
): Response {
val user = securityIdentity.principal as AuthUser
// Check idempotency
val existing = engagementService.findByIdempotencyKey(idempotencyKey)
if (existing != null) {
return Response.ok(existing.toResponse()).build()
}
// Validate user has access to organisation/site
if (!user.hasAccessToOrganisation(request.organisationId)) {
throw AuthScopeViolationException("Cannot submit to this organisation")
}
// Create engagement
val engagement = engagementService.create(
request = request,
userId = user.id,
tenantId = user.tenantId,
idempotencyKey = idempotencyKey
)
return Response.status(201).entity(engagement.toResponse()).build()
}
@POST
@Path("/grievances")
@RolesAllowed("COLLECTOR")
fun createGrievance(
@HeaderParam("Idempotency-Key") idempotencyKey: String,
@Valid request: CreateGrievanceRequest
): Response {
val user = securityIdentity.principal as AuthUser
// Check idempotency
val existing = grievanceService.findByIdempotencyKey(idempotencyKey)
if (existing != null) {
return Response.ok(existing.toResponse()).build()
}
// PII validation
if (validationService.containsPII(request.stakeholderGroup)) {
throw ValidationPIIDetectedException(
field = "stakeholderGroup",
message = "Use general group labels instead of individual names"
)
}
// Validate user has access to organisation/site
if (!user.hasAccessToOrganisation(request.organisationId)) {
throw AuthScopeViolationException("Cannot submit to this organisation")
}
// Create grievance
val grievance = grievanceService.create(
request = request,
userId = user.id,
tenantId = user.tenantId,
idempotencyKey = idempotencyKey
)
return Response.status(201).entity(grievance.toResponse()).build()
}
}
PII Validation Service:
import java.util.regex.Pattern
class ValidationService {
private val namePatterns = listOf(
Pattern.compile("\\b[A-Z][a-z]+ [A-Z][a-z]+\\b"), // "John Smith"
Pattern.compile("\\bMr\\.? [A-Z][a-z]+"), // "Mr. Ahmed"
Pattern.compile("\\bMs\\.? [A-Z][a-z]+"), // "Ms. Jane"
Pattern.compile("\\bMrs\\.? [A-Z][a-z]+"), // "Mrs. Smith"
Pattern.compile("\\bDr\\.? [A-Z][a-z]+") // "Dr. Johnson"
)
fun containsPII(stakeholderGroup: String): Boolean {
return namePatterns.any { pattern ->
pattern.matcher(stakeholderGroup).find()
}
}
}
Cross-References
- API Overview - Authentication, versioning, rate limiting
- Collector API - Evidence upload endpoints
- Error Handling - Standard error codes and responses
- Report Outputs - Stakeholder engagement report format
- Data Model - Database schema for stakeholder engagement
- Collection Templates - Engagement and grievance templates
- RBAC Matrix - Role-based access control rules
Change Log
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-01-17 | Ralph (AI Agent) | Initial stakeholder engagement API documentation for GRI 2-29 reporting |