Media
Upload, retrieve, update, and delete media files. Manage media placeholders (organizational groupings for media items).
All endpoints require authentication via the X-API-Key header. See Authentication.
List media
GET /media
List media with filtering and offset-based pagination. Returns non-deleted media only.
Query parameters
| Parameter | Type | Default | Notes |
|---|---|---|---|
page | number | — | 0-indexed. |
perPage | number | — | Items per page. |
type | string | — | Comma-separated list of: image, video, pdf, share, vcard. |
placeholderId | string | — | Filter by media placeholder group. |
contactId | string | — | Filter by associated contact. |
isPlaceholder | boolean | — | Filter media with or without a placeholder. |
archiveStatus | string | active | One of: archived, active, all. |
Example request
Response
Get a media item
GET /media/:id
Returns a single media item with populated relationships (owner, associated contact, share, placeholder).
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Media ID. |
Example request
Response
Errors
| Status | Message | Cause |
|---|---|---|
| 404 | Media not found | No media with that ID, or it has been deleted. |
Update media metadata
PUT /media/:id
Update a media item's metadata. All fields are optional — only provided fields are updated.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Media ID. |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | No | Display name. |
userId | string | null | No | Owner user ID. Must exist in your org. |
contactId | string | null | No | Associated contact ID. Must exist in your org. |
placeholderId | string | null | No | Placeholder group ID. Must exist in your org. Moves media between groups and updates counts. |
archived | boolean | No | Archive or unarchive. |
summary | string | No | Summary text. |
transcription | string | No | Transcription text. |
Example request
Response
Errors
| Status | Message | Cause |
|---|---|---|
| 400 | Validation error | Invalid user, contact, or placeholder ID. |
Delete a media item
DELETE /media/:id
Soft-deletes a media item: marks the record as deleted and removes files from Google Cloud Storage (both original and thumbnail).
Cannot delete media that a live message still needs, which means a message that is currently sending or building, or one with a future send time that has not been cancelled. Media reached through a placeholder counts too: if a message is attached to the placeholder this media belongs to, the media is protected.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Media ID. |
Example request
Response
Errors
| Status | Message | Cause |
|---|---|---|
| 400 | Media is being used in a message that is scheduled or currently sending | A live message still needs this media, directly or through its placeholder. |
| 404 | Media does not exist | No media with that ID in your org, or it was already deleted. |
Bulk delete media
POST /media/bulk-delete
Soft-deletes multiple media items in one request. Body carries the target IDs (preferred over DELETE with a body, which many proxies strip).
Partial by design: this always returns 200, and every requested ID comes back in exactly one of deletedIds, skippedIds or notFoundIds. Media still needed by a message that is scheduled or currently sending is skipped rather than deleted, including media reached through a placeholder that a message is attached to.
Request body
| Field | Type | Description |
|---|---|---|
mediaIds | string[] | Media IDs to delete (1 to 1000). |
Example request
Response
| Field | Type | Description |
|---|---|---|
deletedCount | number | How many were soft-deleted. Equals deletedIds.length. |
deletedIds | string[] | Soft-deleted; their files were removed from storage. |
skippedIds | string[] | Still needed by a scheduled or in-flight message, so left untouched. |
notFoundIds | string[] | Not in your org, or already deleted. |
Errors
| Status | Message | Cause |
|---|---|---|
| 400 | Validation error | mediaIds is empty, has more than 1000 entries, or contains a malformed ID. |
Upload a media file
POST /media
Upload a media file using multipart form data (not JSON). Metadata is passed as a JSON string in the data field.
File size limit: effectively 32 MiB. Cloud Run caps an HTTP/1 request at that size and refuses anything larger before it reaches this service. For larger files, use the direct upload flow below (POST /media/upload/init), which accepts up to 512MB.
Supported file types
| Category | MIME types |
|---|---|
| Images | image/gif, image/jpeg, image/jpg, image/png, image/webp |
| Videos | video/mp4, video/quicktime (.mov) |
| Documents | application/pdf |
| vCards | text/vcard, text/x-vcard |
The file's content is verified against its declared type. A request whose bytes do not match
(for example an HTML document declared as image/png) is refused with 400 and nothing is stored.
Form fields
| Field | Type | Required | Notes |
|---|---|---|---|
file | file | Yes | The media file to upload. |
thumbnail | file | No | Custom thumbnail image (PNG or JPEG). If omitted, the original file is used as thumbnail for images. |
data | string | Yes | JSON string with metadata: { "name": string, "userId?": string, "contactId?": string, "placeholderId?": string }. |
Image compression
Images over 3.5MB are automatically compressed server-side:
- JPEG: Compressed with MozJPEG at quality 30.
- PNG: Compressed with PNGQuant at quality 30, 256 colors.
The stored file may differ from what was uploaded due to this compression.
Example request
Response
Errors
| Status | Message | Cause |
|---|---|---|
| 400 | Validation error | Invalid file type, missing data field, or invalid metadata. |
Initialize direct upload
POST /media/upload/init
Create a signed upload URL and temporary uploadId for direct-to-storage upload flows.
Direct upload flow
- Call
POST /media/upload/initto get a signedurl,uploadId, andexpiresAt. - Upload the raw file bytes with
PUTto the signedurl(outside your API domain). - Call
POST /media/upload/finalizewith theuploadIdto create the media record.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | Original file name. |
size | number | Yes | File size in bytes. Must be greater than 0. |
type | string | Yes | MIME type. Must be one of the supported file types: image/jpeg (or image/jpg), image/png, image/gif, image/webp, video/mp4, video/quicktime (or video/mov), application/pdf, text/vcard (or text/x-vcard). |
Example request
Response
expiresAt is the timestamp when this upload session expires. Complete the PUT upload and /upload/finalize before this time.
File size limit: 512MB, enforced at init (declared size) and at finalize (actual object size).
This differs from POST /media, which is effectively capped at 32 MiB: Cloud Run refuses a
larger HTTP/1 request before it reaches the service. Use the signed-URL flow for anything bigger.
That platform limit is the reason this flow exists.
The response may include requiredHeaders, an object of headers to send verbatim on the PUT to
the signed URL. It is optional and only present when the server has that feature enabled, but when
present the headers must be included: they are part of the URL's signature, and a PUT without
them is refused by the storage bucket.
Step 2 example: PUT file to signed URL
Use the url returned by init and upload the file bytes directly. The Content-Type should match the type sent in init.
If the upload succeeds, Google Cloud Storage returns 200 OK (or 201 Created) with an empty body.
Finalize direct upload
POST /media/upload/finalize
Finalize a direct upload using the uploadId returned by upload init and create the media record.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
uploadId | string | Yes | Upload session ID returned by init. |
name | string | No | Optional display name. |
contactId | string | null | No | Associated contact ID. |
placeholderId | string | null | No | Placeholder group ID. |
userId | string | null | No | Owner user ID. Defaults to authenticated user when omitted. |
Example request
Response
Upload a thumbnail
POST /media/upload/thumbnail/:mediaId
Upload a thumbnail image for an existing media item. Sends raw binary data (not multipart). Typically used for video thumbnails.
Path parameters
| Parameter | Type | Description |
|---|---|---|
mediaId | string | Media ID to attach the thumbnail to. |
Required headers
| Header | Value | Notes |
|---|---|---|
Content-Type | image/png or image/jpg | Must be a supported image type. |
Content-Length | number | File size in bytes. |
Example request
Response
Errors
| Status | Message | Cause |
|---|---|---|
| 401 | Media not found | No media with that ID in your org. |
| 500 | Invalid media ID | Invalid ObjectId format. |
Start media transcription
POST /media/transcribe/:id
Trigger on-demand transcription for a media item. Only video media is supported.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Media ID. |
Example request
Response
Errors
| Status | Message | Cause |
|---|---|---|
| 400 | Media is not a video | Requested media is not a video MIME type. |
| 400 | Transcription already done | Media already has completed transcription. |
| 400 | Transcription is already in progress | A pending transcription was started less than one hour ago. |
| 404 | Media not found | No media with that ID exists. |
List media placeholders
GET /media/placeholders
List media placeholders (organizational groupings). Each placeholder includes a preview array with up to 3 media items.
Query parameters
| Parameter | Type | Default | Notes |
|---|---|---|---|
search | string | — | Text search on placeholder name. |
includeEmpty | boolean | false | Include placeholders with no media. |
page | number | 0 | 0-indexed. |
perPage | number | 50 | Items per page. |
Example request
Response
Get a media placeholder
GET /media/placeholders/:id
Returns a single media placeholder with a preview of up to 3 media items.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Placeholder ID. |
Example request
Response
Errors
| Status | Message | Cause |
|---|---|---|
| 403 | Placeholder not found | No placeholder with that ID in your org. |
Response envelope
All endpoints use the standard envelope:
Error responses:
data is null for a general failure, such as a database error or an upstream timeout. When the request is rejected because of a specific field, data carries the field(s) at fault instead:
Each issue carries a path (the dotted path into the request body) and a message. A label is added when the path is not self-describing, which today means custom fields only, for example "path": "fields.68b0a1f39c2e4d0012ab77e5", "label": "Class Year". issues is an array, but treat it as holding at most one entry: validation stops at the first failure.
mediaDTO schema
Used by endpoints that return media with populated relationships, such as GET /media/:id and GET /media.