Posts
Managing posts via the Vibescaling API
List Posts
GET /api/v1/posts?status=draft&page=1&per_page=20Query Parameters
| Parameter | Type | Description |
|---|---|---|
| status | string | Filter by status: draft, scheduled, posted, archived |
| post_type | string | Filter by type: slides or video |
| tiktok_account_id | string | Filter by TikTok account UUID |
| page | number | Page number (default: 1, max: 1000) |
| per_page | number | Items per page (default: 20, max: 100) |
Response
{
"data": [...],
"pagination": {
"page": 1,
"per_page": 20,
"total": 42,
"total_pages": 3
}
}Create a Post
POST /api/v1/postsRequest body
{
"title": "My Post Title",
"post_type": "slides",
"caption": "Check out these slides!",
"hashtags": "#tiktok #content",
"tiktok_account_id": "uuid"
}| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Post title (1–500 chars) |
post_type | string | No | "slides" (default) or "video" |
caption | string | No | Post caption (max 2200 chars) |
hashtags | string | No | Hashtags (max 500 chars) |
tiktok_account_id | uuid | No | TikTok account for publishing |
Posts are created with status draft.
Get a Post
GET /api/v1/posts/:idUpdate a Post
PATCH /api/v1/posts/:idRequest body
Any combination of:
{
"title": "Updated title",
"caption": "New caption",
"hashtags": "#updated",
"status": "scheduled",
"scheduled_at": "2026-03-15T14:00:00Z",
"tiktok_account_id": "uuid"
}Scheduling via PATCH
Set status to scheduled along with scheduled_at and tiktok_account_id to schedule a post.
Delete a Post
DELETE /api/v1/posts/:idPermanently deletes a post, its assets (R2 objects), and dispatch logs. Only posts with draft or archived status can be deleted. Returns 204 No Content on success.
Batch Archive
Archive multiple posts at once:
PATCH /api/v1/posts/batchRequest body
{
"ids": ["uuid-1", "uuid-2", "uuid-3"],
"action": "archive"
}| Field | Type | Description |
|---|---|---|
| ids | uuid[] | Post IDs to archive (1–50) |
| action | string | "archive" |
Response
{
"data": { "archived": 3 }
}Only posts belonging to your organization are affected. IDs that don't exist or belong to another org are silently skipped.
Upload Assets
POST /api/v1/posts/:id/assetsUpload slide images or video files for a post.
Request body (slide)
{
"filename": "slide1.png",
"content_type": "image/png"
}Request body (video)
{
"filename": "video.mp4",
"content_type": "video/mp4",
"content_length": 52428800,
"replace": false
}| Field | Type | Description |
|---|---|---|
| filename | string | File name |
| content_type | string | MIME type (image/png, image/jpeg, video/mp4, video/webm, video/quicktime) |
| content_length | number? | File size in bytes (required for validation on large videos) |
| replace | boolean? | Replace existing video (default: false) |
Response
{
"upload_url": "https://...",
"r2_key": "orgs/{org}/posts/{post}/slides/slide1.png"
}Upload the file directly to upload_url via PUT request with the correct Content-Type header.
Video-specific behavior
- Each video post can have only one video. Uploading a second video returns
409unlessreplace: trueis set. - Video content types must start with
video/(e.g.video/mp4,video/webm,video/quicktime). - Maximum video file size is 4GB. Exceeding this returns
413. - Video presigned URLs expire after 2 hours (slides expire after 1 hour).
Replace a video
To replace an existing video, set replace: true:
{
"filename": "updated-video.mp4",
"content_type": "video/mp4",
"replace": true
}This atomically replaces the previous video file.
List Assets
GET /api/v1/posts/:id/assetsReturns the list of assets for a post with presigned download URLs.
Response
{
"data": [
{
"r2_key": "orgs/{org}/posts/{post}/slides/slide1.png",
"url": "https://...",
"content_type": "image/png"
}
]
}Post Object
| Field | Type | Description |
|---|---|---|
| id | uuid | Post ID |
| org_id | uuid | Organization ID |
| post_type | string | slides or video |
| title | string? | Post title |
| caption | string? | Post caption |
| hashtags | string? | Hashtags |
| status | string | draft, scheduled, publishing, posted, archived |
| scheduled_at | timestamp? | When the post is scheduled to publish |
| posted_at | timestamp? | When the post was published |
| tiktok_account_id | uuid? | TikTok account for publishing |
| publish_mode | string | direct or inbox |
| publish_retries | number | Number of publish retry attempts |
| last_publish_error | string? | Last publish failure message |
| privacy_level | string? | TikTok privacy level |
| disable_comment | boolean | Comments disabled |
| disable_duet | boolean | Duets disabled |
| disable_stitch | boolean | Stitching disabled |
| brand_content_toggle | boolean | Branded content disclosure |
| brand_organic_toggle | boolean | Own brand promotion disclosure |
| created_at | timestamp | Creation time |
| updated_at | timestamp | Last update time |