Media
Issues can have media attachments β images, videos, or Twitter/X post URLs. Images and videos are stored in S3 using a presigned URL upload flow that bypasses the application server for the actual file transfer.
Supported Media Typesβ
| Type | MIME Types | Max Size | Notes |
|---|---|---|---|
| Image | image/jpeg, image/png, image/webp, image/gif | 10 MB | Stored in S3 |
| Video | video/mp4, video/webm, video/quicktime | 100 MB | Stored in S3 |
| Twitter / X | n/a | β | URL is stored as a reference, not a file |
An issue can have up to 10 media items in total.
POST /api/issues/:id/mediaβ
Add a media attachment to an issue. This endpoint handles both Twitter URL attachments and S3 presigned upload requests. Authentication required.
Attaching a Twitter / X URLβ
Request:
POST /api/issues/iss_01HN5K.../media
Authorization: Bearer <accessToken>
Content-Type: application/json
{
"type": "twitter",
"url": "https://twitter.com/BBMPBangalore/status/1234567890"
}
Response β 201 Created:
{
"success": true,
"data": {
"mediaId": "med_01HP...",
"type": "twitter",
"url": "https://twitter.com/BBMPBangalore/status/1234567890",
"addedAt": "2025-01-15T11:00:00.000Z"
}
}
Requesting a Presigned Upload URLβ
To upload an image or video, first request a presigned URL from the API. The actual file upload goes directly from the browser to S3 β the application server is not involved in the transfer.
Step 1 β Request presigned URL:
POST /api/issues/iss_01HN5K.../media
Authorization: Bearer <accessToken>
Content-Type: application/json
{
"type": "upload",
"mimeType": "image/jpeg",
"fileName": "broken-streetlight.jpg",
"fileSize": 2457600
}
Validation:
mimeType: must be one of the supported MIME types listed abovefileSize: must be provided, enforced against per-type limits
Response β 200 OK:
{
"success": true,
"data": {
"mediaId": "med_01HQ...",
"uploadUrl": "https://civicpulse-media.s3.ap-south-1.amazonaws.com/issues/iss_01HN5K.../med_01HQ....jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Date=...&X-Amz-Expires=900&X-Amz-SignedHeaders=content-type%3Bhost&X-Amz-Signature=...",
"expiresAt": "2025-01-15T11:15:00.000Z",
"publicUrl": "https://civicpulse-media.s3.ap-south-1.amazonaws.com/issues/iss_01HN5K.../med_01HQ....jpg"
}
}
Step 2 β Upload directly to S3:
const { uploadUrl, mediaId, publicUrl } = response.data;
await fetch(uploadUrl, {
method: 'PUT',
headers: {
'Content-Type': 'image/jpeg',
},
body: fileBlob,
});
The presigned URL is valid for 15 minutes. The Content-Type header in the PUT request must exactly match the mimeType you specified in Step 1, or S3 will reject the upload with a 403 SignatureDoesNotMatch error.
Step 3 β The media item is automatically available.
The mediaId is already stored against the issue record when the presigned URL is issued. The publicUrl returned in Step 1 will serve the file once the S3 upload completes.
S3 Presigned URL Flow Diagramβ
Client CivicPulse API AWS S3
β β β
β POST /media {mimeType} β β
β βββββββββββββββββββββββββββββΊβ β
β β GeneratePresignedUrl β
β β βββββββββββββββββββββββΊβ
β β βββ presigned URL βββββ
β βββ { uploadUrl, mediaId } ββ β
β β β
β PUT <uploadUrl> (file data) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββΊ β
β βββ 200 OK ββββββββββββββββββββββββββββββββββββββββββ
β β β
β (file now publicly accessible at publicUrl) β
DELETE /api/issues/:id/media/:mediaIdβ
Remove a media attachment from an issue. Authentication required. The requesting user must be the issue reporter or an admin.
For S3 media, the object is also deleted from the bucket. For Twitter URLs, only the reference record is removed.
Request:
DELETE /api/issues/iss_01HN5K.../media/med_01HQ...
Authorization: Bearer <accessToken>
Response β 200 OK:
{
"success": true,
"data": {
"mediaId": "med_01HQ...",
"deleted": true
}
}
Response β 403 (not the reporter or admin):
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "You do not have permission to remove this media item"
}
}
Notesβ
- Presigned URLs are single-use for the same file size and content type. If the upload fails, request a new presigned URL.
- Files uploaded to S3 are private by default; the presigned URL flow is the only supported upload mechanism β direct bucket access is not granted.
- GIF images are accepted but are not animated in the UI; they are displayed as static images.
- The S3 bucket is configured with a CORS policy that allows
PUTfrom the configuredCORS_ORIGINorigins. See Deployment for the full CORS configuration.