API Documentation
Complete reference for the PostPulse API - unified publishing to 9 social media platforms.
Authentication
PostPulse API uses OAuth 2.0 for authentication. Sign up at PostPulse and create an OAuth application at PostPulse Developer Portal to get your API credentials instantly.
API Base URLs
Authentication: https://auth.post-pulse.com
API Endpoints: https://api.post-pulse.comOAuth Flow
// Step 1: Get authorization code
GET https://auth.post-pulse.com/authorize
?client_id=YOUR_CLIENT_ID
&response_type=code
&redirect_uri=YOUR_REDIRECT_URI
&scope=postpulse-api/accounts.read postpulse-api/api postpulse-api/media.write postpulse-api/posts.read postpulse-api/posts.write postpulse-api/webhooks.write
&state=YOUR_STATE_PARAMETER
&audience=https://api.post-pulse.com
// Step 2: Exchange code for access token
POST https://auth.post-pulse.com/oauth/token
Content-Type: application/json
{
"grant_type": "authorization_code",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"code": "AUTHORIZATION_CODE",
"redirect_uri": "YOUR_REDIRECT_URI"
}Using Access Token
All API requests must include the Bearer token in the Authorization header:
Authorization: Bearer YOUR_ACCESS_TOKENAccounts
Retrieve connected social media accounts for the authenticated user.
Get All Accounts
Returns a list of all social media accounts connected by the user.
Response Example
[
{
"id": 123,
"platform": "INSTAGRAM",
"accountId": "instagram_user_id",
"accountUsername": "myaccount",
"accountDisplayName": "My Instagram Account",
"accountAvatarUrl": "https://example.com/avatar.jpg",
"createdTime": "2025-08-15T10:30:00.000000",
"updatedTime": "2025-08-15T10:30:00.000000"
}
]Get Connected Chats
For platforms like Telegram, retrieve available channels/chats where the bot can publish.
Response Example
[
{
"id": "-1002726180643",
"title": "PostPulse Official",
"type": "channel",
"platform": "TELEGRAM"
}
]Media Upload
Most platforms require media to be hosted in PostPulse storage before scheduling. You have three ways to get media there — pick the one that fits your use case.
Direct Upload
POST /v1/media/upload
Send the file as binary in a single multipart/form-data request.
- Simplest approach
- Best for small files
- Synchronous — returns the media key immediately
Pre-signed PUT URL
POST /v1/media/upload/urls
Request a pre-signed URL, then PUT the file directly to our storage.
- Best for large files
- Bypasses API size limits
- Confirm step recommended after upload
Public URL Import
POST /v1/media/upload/import
Provide a public URL — we download the file into our storage.
- Fully asynchronous
- Poll for completion
- Or pass the URL directly to
attachmentPaths
Direct Upload
Upload an image or video file in a single request.
Request
Content-Type: multipart/form-data
file: [binary file data]Response Example
{
"path": "6354d8d2-e0f1-702b-af6c-62e28e377ec7/e1992700-96f5-4f0b-bd48-45d902ffbb0c.jpeg"
}Use the returned path as an entry in attachmentPaths when creating a post.
Pre-signed PUT URL
For large files, request a pre-signed URL and upload the binary directly to our storage. This takes three calls: request URL → PUT file → confirm upload.
1. Request the URL
Get a pre-signed URL configured for your file's filename, content type, and size.
Request Schema
{
"filename": "my-video.mp4",
"contentType": "video/mp4",
"sizeBytes": 10485760
}Response Schema
{
"key": "f157080a-7c45-4b43-9d7c-d17dd83dbf92/b43ec45c-6367-4edd-868b-5f704a4ff04e.mp4",
"method": "PUT",
"url": "https://social-media-planner-s3.fly.storage.tigris.dev/f157080a-7c45-4b43-9d7c-d17dd83dbf92/b43ec45c-6367-4edd-868b-5f704a4ff04e.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20260418T193733Z&X-Amz-SignedHeaders=content-type%3Bhost%3Bx-amz-meta-declared-size%3Bx-amz-meta-orig-filename&X-Amz-Credential=...&X-Amz-Expires=600&X-Amz-Signature=...",
"headers": {
"Content-Type": "video/mp4",
"x-amz-meta-declared-size": "10485760",
"x-amz-meta-orig-filename": "my-video.mp4"
},
"expiresAt": "2026-04-18T19:47:33Z"
}url with the exact headers and HTTP method from the response — the signature is bound to them.2. PUT the file to the returned URL
PUT https://social-media-planner-s3.fly.storage.tigris.dev/...?X-Amz-Signature=...
Content-Type: video/mp4
x-amz-meta-declared-size: 10485760
x-amz-meta-orig-filename: my-video.mp4
[binary file bytes]Storage responds with 200 OK on success. The URL expires after the window shown in expiresAt.
3. Confirm the upload (recommended)
Confirms the file landed in our storage and runs a basic integrity check (for videos, this verifies the container is readable). Optional but recommended — catches transfer problems early and avoids surprises at post time.
Request Schema
{
"key": "f157080a-7c45-4b43-9d7c-d17dd83dbf92/b43ec45c-6367-4edd-868b-5f704a4ff04e.mp4"
}Response
Returns 204 No Content on success. On integrity failure, returns a 4xx with the problem described in the body.
Public URL Import
Already have a public URL? Hand it to us and we'll pull the file into storage for you. This is fully asynchronous — you get an import job back and poll for completion.
attachmentPaths when creating a post — see the Post Scheduling section. The platform will create the import job for you and flip the post to SCHEDULED once the file is ready.Start an import
Kick off a background import from a public URL.
Request Schema
{
"url": "https://example.com/image.jpg",
"filenameHint": "image.jpg"
}Response Schema
{
"id": 12345,
"state": "QUEUED"
}Poll import status
Get the current status of a media import job.
Response Schema
{
"id": 12345,
"state": "READY", // QUEUED, DOWNLOADING, VALIDATING, UPLOADING, READY, FAILED_TEMPORARY, FAILED_PERMANENT
"sourceUrl": "https://example.com/image.jpg",
"bytesDownloaded": 2048576,
"totalBytes": 2048576,
"detectedMime": "image/jpeg",
"s3Key": "6354d8d2-e0f1-702b-af6c-62e28e377ec7/e1992700-96f5-4f0b-bd48-45d902ffbb0c.jpeg",
"errorCode": null,
"errorMessage": null
}When state is READY, use s3Key as the attachmentPaths entry. FAILED_PERMANENT means the job is dead — inspect errorCode and errorMessage. FAILED_TEMPORARY may still recover.
media.upload.ready event when the import is complete.Post Scheduling
Schedule posts to one or multiple social media platforms simultaneously.
Understanding the Data Model
PostSchedule
The root object that contains common settings for all publications:
- scheduledTime: When to publish. Accepts ISO 8601 with or without timezone offset (see below).
- isDraft: Whether this is a draft (recommended: false for API)
- publications: Array of Publication objects
Publication
Represents publishing to one social media account:
- accountId: ID from the accounts endpoint
- platformSettings: Platform-specific configuration
- posts: Array of Post objects
Post
Individual post content:
- content: Post text/description
- attachmentPaths: Array of media references — each entry is either an s3 key returned by a media upload, or a public URL we should import.
- chatId: For Telegram only - specific channel ID
- thumbnailPath: Optional thumbnail (coming soon)
Scheduled Time Formats
The scheduledTime field accepts ISO 8601 with or without a timezone offset. All values are normalized to UTC on the server.
// All three formats are accepted:
"2026-04-17T23:33:00" // No offset — interpreted as UTC
"2026-04-17T23:33:00Z" // Explicit UTC
"2026-04-17T19:33:00-04:00" // With offset — normalized to UTC server-sideAttachments: Key vs Public URL
Each entry in attachmentPaths can be one of two things:
Post Lifecycle
The diagram below shows how a posting schedule moves through statuses. Posts with s3 keys go straight to SCHEDULED. Posts with public URLs start in AWAITING_MEDIA and transition to SCHEDULED once every referenced file has been imported — or to MEDIA_IMPORT_FAILED if any import fails permanently.

Create Post Schedule
Schedule a post to one or multiple platforms.
Request Example — Storage Key
{
"scheduledTime": "2026-04-17T19:33:00-04:00",
"isDraft": false,
"publications": [
{
"socialMediaAccountId": 445,
"platformSettings": {
"type": "TELEGRAM"
},
"posts": [
{
"content": "Ready to go!",
"chatId": "-1001947450140",
"attachmentPaths": [
"6354d8d2-e0f1-702b-af6c-62e28e377ec7/e1992700-96f5-4f0b-bd48-45d902ffbb0c.jpeg"
]
}
]
}
]
}Request Example — Public URL
{
"scheduledTime": "2026-04-20T12:00:00Z",
"isDraft": false,
"publications": [
{
"socialMediaAccountId": 445,
"platformSettings": { "type": "TELEGRAM" },
"posts": [
{
"content": "Posting from a remote image",
"chatId": "-1001947450140",
"attachmentPaths": [
"https://example.com/my-image.jpg"
]
}
]
}
]
}Response
Returns 201 Created with the newly created posting schedule:
{
"id": 9821,
"overallStatus": "SCHEDULED", // SCHEDULED | AWAITING_MEDIA | DRAFT
"scheduledTime": "2026-04-17T23:33:00Z",
"publications": [
{
"id": 14433,
"socialMediaAccountId": 445,
"platformSettings": { "type": "TELEGRAM" },
"posts": [
{
"id": 28801,
"chatId": "-1001947450140",
"content": "Ready to go!",
"attachmentPaths": [
"6354d8d2-e0f1-702b-af6c-62e28e377ec7/e1992700-96f5-4f0b-bd48-45d902ffbb0c.jpeg"
],
"thumbnail": null
}
]
}
],
"pendingImports": [] // Populated when attachmentPaths contain public URLs
}When any attachmentPaths entry is a public URL, overallStatus is AWAITING_MEDIA and pendingImports is populated with one entry per in-flight import. Track progress by polling GET /v1/media/upload/import/{importTaskId}:
"pendingImports": [
{
"postId": 28801,
"attachmentIndex": 0,
"importTaskId": 12345,
"sourceUrl": "https://example.com/my-image.jpg",
"state": "QUEUED" // Poll GET /v1/media/upload/import/{importTaskId}
}
]Platform Settings
Each platform requires specific settings in the platformSettings JSON object. The type field is mandatory for all platforms and must match the platform name.
platformSettings field with the type value in your request.{
"type": "INSTAGRAM",
"publicationType": "FEED" | "REELS" | "STORY"
}{
"type": "LINKEDIN",
"visibility": "PUBLIC" | "CONNECTIONS" | "GROUP"
}Threads
{
"type": "THREADS",
"topicTag": "optional topic tag"
}TikTok
{
"type": "TIK_TOK",
"title": "Video title",
"privacyLevel": "PUBLIC_TO_EVERYONE" | "FOLLOWER_OF_CREATOR" | "MUTUAL_FOLLOW_FRIENDS" | "SELF_ONLY",
"disableDuet": false,
"disableComments": false,
"disableStitch": false,
"brandContent": false,
"brandOrganic": false,
"hasUsageConfirmation": true
}YouTube
{
"type": "YOUTUBE",
"title": "Video title",
"privacyStatus": "PUBLIC" | "UNLISTED" | "PRIVATE",
"category": "FILM_ANIMATION" | "AUTOS_VEHICLES" | "MUSIC" | "PETS_ANIMALS" | "SPORTS" | "TRAVEL_EVENTS" | "GAMING" | "PEOPLE_BLOGS" | "COMEDY" | "ENTERTAINMENT" | "NEWS_POLITICS" | "HOWTO_STYLE" | "EDUCATION" | "SCIENCE_TECHNOLOGY"
}Telegram
{
"type": "TELEGRAM"
}X (Twitter)
{
"type": "TWITTER"
}Bluesky
{
"type": "BLUE_SKY"
}{
"type": "FACEBOOK",
"publicationType": "FEED" | "REELS" | "STORY"
}Data Models
Account Model
{
"id": "number",
"platform": "X_TWITTER" | "YOUTUBE" | "THREADS" | "TIKTOK" | "INSTAGRAM" | "LINKEDIN" | "BLUE_SKY" | "TELEGRAM" | "FACEBOOK",
"accountId": "string",
"accountUsername": "string",
"accountDisplayName": "string",
"accountAvatarUrl": "string",
"createdTime": "ISO 8601 datetime",
"updatedTime": "ISO 8601 datetime"
}Chat Model
{
"id": "string",
"title": "string",
"type": "string",
"platform": "string"
}Media Upload Response
{
"path": "string"
}Publication Model
{
"socialMediaAccountId": "number",
"platformSettings": {
"type": "platform_enum"
},
"posts": [
{
"content": "string",
"chatId": "string (optional, for Telegram)",
"attachmentPaths": ["string — storage key (from media upload) OR public URL"]
}
]
}Posting Schedule Created Response
Returned from POST /v1/posts.
{
"id": "number",
"overallStatus": "SCHEDULED | AWAITING_MEDIA | DRAFT",
"scheduledTime": "ISO 8601 datetime (UTC)",
"publications": [
{
"id": "number",
"socialMediaAccountId": "number",
"platformSettings": { "type": "platform_enum" },
"posts": [
{
"id": "number",
"chatId": "string | null",
"content": "string",
"attachmentPaths": ["string"],
"thumbnail": "string | null"
}
]
}
],
"pendingImports": [
{
"postId": "number",
"attachmentIndex": "number",
"importTaskId": "number",
"sourceUrl": "string",
"state": "QUEUED | DOWNLOADING | VALIDATING | UPLOADING | READY | FAILED_TEMPORARY | FAILED_PERMANENT"
}
]
}Error Handling
PostPulse API uses standard HTTP status codes and returns detailed error information.
Error Response Format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": [
{
"field": "publications[0].platformSettings.type",
"message": "Required field missing"
}
]
}
}Common Error Codes
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Request data validation failed |
| 401 | UNAUTHORIZED | Invalid or missing authentication |
| 403 | FORBIDDEN | Insufficient permissions |
| 404 | NOT_FOUND | Resource not found |
| 429 | RATE_LIMITED | Too many requests |
| 500 | INTERNAL_ERROR | Server error |