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.com

OAuth 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_TOKEN

Accounts

Retrieve connected social media accounts for the authenticated user.

Get All Accounts

GET/v1/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

GET/v1/accounts/{id}/chats?platform={platform}

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.

1

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
2

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
3

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

POST/v1/media/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.

Your ClientPostPulse APIObject StoragePOST /v1/media/upload/urls{ filename, contentType, sizeBytes }200 OK{ key, url, method, headers, expiresAt }PUT {url} — binary file(with headers from previous response)200 OKPOST /v1/media/upload/confirm (recommended){ key }204 No Content

1. Request the URL

POST/v1/media/upload/urls

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"
}
Important: You must send the request to the returned 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)

POST/v1/media/upload/confirm

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.

Your ClientPostPulse APISource URLPOST /v1/media/upload/import{ url, filenameHint }202 Accepted{ id, state: "QUEUED" }GET (background download)GET /v1/media/upload/import/{id} (poll)200 OK{ state: "READY", s3Key, ... }
Shortcut: You can also pass a public URL directly as an entry in 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

POST/v1/media/upload/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/v1/media/upload/import/{id}

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.

Tip: Instead of polling, you can use Webhooks to receive a 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-side

Attachments: Key vs Public URL

Each entry in attachmentPaths can be one of two things:

Storage key (path)

Returned from any of the Media Upload endpoints. The post is created in SCHEDULED status immediately.

"attachmentPaths": [
  "6354d8d2-.../e1992700-...jpeg"
]

Public URL

We create an import job and flip the post to SCHEDULED once the file is ready. If the import fails permanently, the post moves to MEDIA_IMPORT_FAILED.

"attachmentPaths": [
  "https://example.com/my-image.jpg"
]

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.

Post schedule lifecycle diagram showing transitions between DRAFT, AWAITING_MEDIA, SCHEDULED, MEDIA_IMPORT_FAILED, IN_PROGRESS, COMPLETED, PARTIALLY_COMPLETED and FAILED states

Create Post Schedule

POST/v1/posts

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.

Important: Even if a platform doesn't have specific settings yet (like X/Twitter), you must still include the platformSettings field with the type value in your request.

Instagram

{
  "type": "INSTAGRAM",
  "publicationType": "FEED" | "REELS" | "STORY"
}

LinkedIn

{
  "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"
}

Facebook

{
  "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 StatusError CodeDescription
400VALIDATION_ERRORRequest data validation failed
401UNAUTHORIZEDInvalid or missing authentication
403FORBIDDENInsufficient permissions
404NOT_FOUNDResource not found
429RATE_LIMITEDToo many requests
500INTERNAL_ERRORServer error