# Introduction [Postpone](https://www.postpone.app){rel=""nofollow""} offers a GraphQL API to query data and perform mutations to schedule social media posts. If you're new to GraphQL then check out [GraphQL.org](https://graphql.org/){rel=""nofollow""}. **Postpone's API URL:** {rel=""nofollow""} ## API Playground The best way to learn Postpone's API is to play around with it in our GraphQL explorer. To do that, log into Postpone in a web browser then navigate to {rel=""nofollow""}. From here you can execute queries and mutations against Postpone. - **Queries** retrieve data, such as a list of scheduled posts or post analytics - **Mutations** change data, such as scheduling a post on Reddit or X/Twitter Try out this GraphQL query to return a list of Reddit posts, each post's submissions, and the results of those submissions. ![Postpone API playground](https://developers.postpone.app/img/api-playground.png) ## Authentication Postpone uses API keys to authenticate requests against the GraphQL API. You can create and manage your API keys from [**Settings > Integrations > Postpone API**](https://www.postpone.app/settings/api){rel=""nofollow""}. For detailed information on authentication, including how to get your API key, security best practices, and troubleshooting, see our [Authentication guide](https://developers.postpone.app/getting-started/authentication). ## Rate Limits To ensure fair usage and system stability, Postpone enforces rate limits on API requests: - **Free accounts:** 25 requests per 10 minutes - **Paid accounts:** 300 requests per 10 minutes Rate limits are applied per user. For more information about rate limits, including headers and best practices, see our [Rate Limits guide](https://developers.postpone.app/getting-started/rate-limits). ## Core Models Postpone has four key models in its API: **Post**, **Submission**, **Result**, and **Error**. ::card-group :::card{icon="i-lucide-file-text" title="Post"} A social media post that is going to be submitted. Contains the post's content, such as title, link, media, comment, and other post settings. A Post has one or more Submissions. ::: :::card{icon="i-lucide-send" title="Submission"} A submission of a social media post at a point in time. ::: :::card{icon="i-lucide-check-circle" title="Result"} Successfully submitted to the platform. ::: :::card{icon="i-lucide-alert-circle" title="Error"} Failed to submit to the platform. ::: :: A Submission has one of the following: a **Result** (successfully submitted to the platform), an **Error** (failed to submit to the platform), or neither (the post is either scheduled or a Draft). ## Error Handling When working with mutations in the Postpone API, it's important to properly handle errors and validation issues. The API provides detailed error information to help you understand what went wrong and how to fix it. For comprehensive information about error handling, validation errors, and best practices, see our dedicated [Error Handling guide](https://developers.postpone.app/getting-started/errors). ## Complete API Request Example Here's a full example of making a request to the Postpone API: ```bash curl --request POST \ --url https://api.postpone.app/gql \ --header 'authorization: Bearer YOUR-API-KEY' \ --header 'content-type: application/json' \ --data '{"query": "query profile { profile { id username email registeredAt } }", "operationName": "profile"}' ``` ## Follow API Updates Check the [API Changelog](https://developers.postpone.app/getting-started/changelog) to keep up with new features and notable changes to the Postpone API. # Authentication Postpone uses personal API key to authenticate requests against the GraphQL API. This guide covers how to obtain your API key, use it in requests, and best practices for API authentication. ## Getting Your API Key To access Postpone's API, you'll need to generate a personal API key from your account settings. ### Step-by-step instructions: 1. Log into your Postpone account at [postpone.app](https://www.postpone.app){rel=""nofollow""} 2. Navigate to [**Settings > Integrations > Postpone API**](https://www.postpone.app/settings/api){rel=""nofollow""} from the main navigation 3. **Create** an API Key. Give it a name and optionally an expiration date. 4. **Copy** the new API key. ::warning Keep your API Key secure and never share it publicly. Anyone with your key can access your Postpone account via the API. :: ## Using Your API Key API Keys must be included in the `Authorization` header of every request to Postpone's API. Use the `Bearer` authentication scheme with your key. ### Header format: ```text Authorization: Bearer YOUR-API-KEY ``` ### Complete request example: ```bash curl --request POST \ --url https://api.postpone.app/gql \ --header 'Authorization: Bearer YOUR-API-KEY' \ --header 'Content-Type: application/json' \ --data '{ "query": "query profile { profile { id username email } }" }' ``` ## Authentication Examples ::code-group ```js [JavaScript/Node.js] const response = await fetch('https://api.postpone.app/gql', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR-API-KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ query: ` query profile { profile { id username email } } ` }) }); const data = await response.json(); ``` ```python [Python] import requests headers = { 'Authorization': 'Bearer YOUR-API-KEY', 'Content-Type': 'application/json', } query = """ query profile { profile { id username email } } """ response = requests.post( 'https://api.postpone.app/gql', headers=headers, json={'query': query} ) data = response.json() ``` :: ## API Key Security Best Practices ### Environment Variables Store your API Key in environment variables rather than hardcoding it in your application: ```bash # .env file POSTPONE_API_KEY=your-api-key-here ``` ```javascript // In your application const apiKey = process.env.POSTPONE_API_KEY; ``` ### Key Rotation Regularly rotate your API Keys for enhanced security: 1. Generate a new key in your Settings 2. Update your applications to use the new key 3. Test that everything works with the new key 4. The old key will be automatically invalidated when you generate a new one ## Troubleshooting Authentication ### Common authentication errors: **401 Unauthorized** - Your API Key is missing, invalid, or expired - Make sure you're including the `Bearer` prefix in the Authorization header **403 Forbidden** - Your account doesn't have access to the API - The key may belong to a different account **Invalid key format** - Ensure you're using `Bearer YOUR-API-KEY` format - Check for extra spaces or characters in your key ### Testing your authentication: Use this simple query to test if your authentication is working: ```bash curl --request POST \ --url https://api.postpone.app/gql \ --header 'Authorization: Bearer YOUR-API-KEY' \ --header 'Content-Type: application/json' \ --data '{"query": "query { profile { username } }"}' ``` A successful response will return your username: ```json { "data": { "profile": { "username": "your-username" } } } ``` # Error Handling GraphQL does not return a 400 response status code when a request is invalid. Instead, Postpone's GraphQL API returns specific attributes for all mutations to help you understand what went wrong and how to fix it. ## Error Response Structure All mutations in the Postpone API return the following attributes: ::field-group :::field{name="success" type="boolean"} Indicates if the mutation was performed successfully. Always check this value first before proceeding. ::: :::field{name="errors" type="ValidationError[]"} A list of validation error objects that provide detailed information about what went wrong. Each object contains `field`, `message`, and `code` properties. ::: :: ## Validation Errors Validation errors provide detailed information about why a mutation failed. Each validation error object contains: ::field-group :::field{name="field" type="string"} The specific field that caused the error. This helps you identify which input field needs to be corrected. ::: :::field{name="message" type="string"} A human-readable description of the error that can be displayed to users or used for debugging. ::: :::field{name="code" type="string"} A machine-readable error code that allows your application to handle specific error types programmatically. ::: :: ### Example: Missing Required Fields When creating a Reddit post without providing required content: ```json { "data": { "scheduleRedditPost": { "success": false, "errors": [ { "field": "content", "message": "Either link or content is required for a Reddit post", "code": "LINK_OR_CONTENT_REQUIRED" } ] } } } ``` ### Example: Invalid Field Values When providing invalid data for specific fields: ```json { "data": { "scheduleRedditPost": { "success": false, "errors": [ { "field": "title", "message": "Title must be between 1 and 300 characters", "code": "INVALID_LENGTH" }, { "field": "scheduledAt", "message": "Scheduled time must be in the future", "code": "INVALID_DATE" } ] } } } ``` ## GraphQL Errors vs Mutation Errors It's important to distinguish between GraphQL-level errors and mutation-level errors: ### GraphQL Errors These occur at the GraphQL parsing/execution level and are returned in the `errors` field of the root response: ```json { "errors": [ { "message": "Cannot query field 'invalidField' on type 'Post'", "locations": [{"line": 3, "column": 5}] } ] } ``` ### Mutation Errors These are business logic errors returned within the mutation's response: ```json { "data": { "scheduleRedditPost": { "success": false, "errors": [ { "field": "title", "message": "Title is required", "code": "REQUIRED_FIELD" } ] } } } ``` Always check both levels when handling API responses. ## Common Error Codes ### Authentication Errors ::field-group :::field{name="emailDoesNotExist" type="string"} We couldn't find a user with that email. Please try again. ::: :::field{name="passwordIncorrect" type="string"} Current password is incorrect. ::: :::field{name="passwordsNotMatch" type="string"} Passwords do not match. ::: :::field{name="wrongToken" type="string"} Password reset link has expired. ::: :::field{name="duplicateEmail" type="string"} That email is already associated with a different Postpone user. Please log in with that email or use a different one. ::: :::field{name="password_too_short" type="string"} Your password must contain at least 8 characters. ::: :::field{name="password_too_common" type="string"} Your password can't be a commonly used password. ::: :::field{name="password_too_similar" type="string"} Your password can't be too similar to your other personal information. ::: :::field{name="password_entirely_numeric" type="string"} Your password can't be entirely numeric. ::: :: ### Reddit Post Errors ::field-group :::field{name="subredditDoesNotExist" type="string"} The subreddit does not exist. ::: :::field{name="linkPostNotAllowed" type="string"} Link posts are not allowed by one or more subreddits. ::: :::field{name="selfPostNotAllowed" type="string"} Self posts are not allowed by one or more subreddits. ::: :::field{name="pollPostNotAllowed" type="string"} Poll posts are not allowed by one or more subreddits. ::: :::field{name="imagePostNotAllowed" type="string"} Image posts are not allowed by one or more subreddits. ::: :::field{name="gifPostNotAllowed" type="string"} GIF posts are not allowed by one or more subreddits. ::: :::field{name="videoPostNotAllowed" type="string"} Video posts are not allowed by one or more subreddits. ::: :::field{name="galleryPostNotAllowed" type="string"} Gallery posts are not allowed by one or more subreddits. ::: :::field{name="notLoggedIn" type="string"} You are not logged in. Login to continue. ::: :::field{name="commentMaxLength" type="string"} Comment cannot be more than 10,000 characters. ::: :::field{name="redditAccountNotLinked" type="string"} That Reddit account has not been linked to Postpone. ::: :::field{name="invalidURL" type="string"} That isn't a valid URL. ::: :::field{name="postOutsideOfPlan" type="string"} Post outside of plan limits for the scheduled month. Upgrade to schedule additional Reddit posts. ::: :::field{name="editScopeNotEnabled" type="string"} Postpone needs elevated permissions to auto-remove posts from this Reddit account. Please log into Postpone again with this Reddit account to proceed. ::: :::field{name="titleTooLong" type="string"} Post titles can't be longer than 300 characters. ::: :::field{name="tooManySubmissions" type="string"} You can only schedule up to a limited number of submissions per post. ::: :::field{name="noSubmissions" type="string"} You must schedule to at least one subreddit. ::: :::field{name="tooManyMediaPerGallery" type="string"} You can only add up to a limited number of media items per gallery. ::: :::field{name="draftWithSubmittedSubmissions" type="string"} You cannot turn a submitted post into a draft. ::: :::field{name="submissionDateInPast" type="string"} Submission date must be in the future. Please select a future date. ::: :::field{name="bulkImportInvalidDateTimeFormat" type="string"} Invalid date/time format in the post_at column for one or more rows. Please use the format YYYY-MM-DD HH\:MM. ::: :::field{name="missingSubmissionId" type="string"} Post is not linked to platform. Please fix it in History and try again. ::: :: # GraphQL Types The Postpone API uses several core GraphQL types to represent different entities in the system. Understanding these types is essential for effectively working with the API. ## Core Types ### UserType The `UserType` represents a user account in the Postpone system with their preferences, permissions, and associated data. ::field-group :::field{required name="id" type="ID!"} Unique identifier for the user. ::: :::field{name="email" type="String"} User's email address. ::: :::field{name="username" type="String"} User's chosen username. ::: :::field{name="fullName" type="String"} User's full display name. ::: :::field{name="timezone" type="String"} User's preferred timezone for scheduling. ::: :::field --- name: socialAccounts type: "[SocialAccountType]" --- List of connected social media accounts the user can access. ::: :::field{name="account" type="AccountType"} The user's account information including plan and billing details. ::: :::field{name="remainingMonthlyPosts" type="Int"} Number of posts remaining in the current billing period. ::: :: ### AccountType The `AccountType` represents a billing account with its plan, limits, and subscription information. ::field-group :::field{required name="id" type="ID!"} Unique identifier for the account. ::: :::field{name="name" type="String"} Display name for the account. ::: :::field{name="plan" type="String"} Current subscription plan name. ::: :::field{name="ownerId" type="ID"} ID of the user who owns this account. ::: :::field{name="subscriptionStatus" type="String"} Current status of the subscription (active, canceled, etc.). ::: :::field{name="allowedMonthlyPosts" type="Int"} Maximum number of posts allowed per month. ::: :::field{name="allowedSocialAccounts" type="Int"} Maximum number of social accounts that can be connected. ::: :::field{name="allowedContentStorage" type="BigInt"} Maximum content storage allowed in bytes. ::: :::field{name="currentPeriodEnd" type="DateTime"} When the current billing period ends. ::: :: ### SocialAccountType The `SocialAccountType` represents a connected social media account with its configuration and capabilities. ::field-group :::field{required name="id" type="ID!"} Unique identifier for the social account. ::: :::field{name="platform" type="String"} Social media platform name (e.g., "reddit", "twitter", "instagram"). ::: :::field{name="username" type="String"} Username on the social platform. ::: :::field{name="formattedUsername" type="String"} Display-friendly version of the username. ::: :::field{name="name" type="String"} Display name on the social platform. ::: :::field{name="avatarUrl" type="String"} URL to the account's profile picture. ::: :::field{name="followers" type="Int"} Number of followers on the platform. ::: :::field{name="isEnabled" type="Boolean"} Whether the account is enabled for posting. ::: :::field{name="isConnected" type="Boolean"} Whether the account is successfully connected. ::: :::field{name="verified" type="Boolean"} Whether the account is verified on the platform. ::: :: ### MediaType The `MediaType` represents uploaded media files in the content library. ::field-group :::field{required name="id" type="ID!"} Unique identifier for the media file. ::: :::field{name="name" type="String"} Display name of the media file. ::: :::field{name="altText" type="String"} Alternative text description for accessibility. ::: :::field{name="url" type="String"} Direct URL to access the media file. ::: :::field{name="mimeType" type="String"} MIME type of the file (e.g., "image/jpeg", "video/mp4"). ::: :::field{name="size" type="BigInt"} File size in bytes. ::: :::field{name="isImage" type="Boolean"} Whether the file is an image. ::: :::field{name="isVideo" type="Boolean"} Whether the file is a video. ::: :::field{name="isGif" type="Boolean"} Whether the file is an animated GIF. ::: :::field{name="thumbnailUrl" type="String"} URL to a thumbnail version of the media. ::: :::field{name="duration" type="Int"} Duration of a video in milliseconds. ::: :::field{name="coverImage" type="CoverImageType"} The video's cover image, or `null` when it has none. See [Video Covers](https://developers.postpone.app/scheduling-posts/video-covers). ::: :::field{name="scheduledPosts" type="Int"} Number of scheduled posts using this media. ::: :::field{name="submittedPosts" type="Int"} Number of submitted posts using this media. ::: :: ## Submission Types ### SocialSubmissionResultType Represents the result of a successful post submission to a social platform. ::field-group :::field{required name="id" type="ID!"} Unique identifier for the submission result. ::: :::field{name="accountUsername" type="String"} Username of the account that made the submission. ::: :::field{name="externalSubmissionId" type="String"} Platform-specific ID for the submitted post. ::: :::field{name="url" type="String"} Direct URL to view the submitted post. ::: :::field{name="dateSubmitted" type="DateTime"} When the post was submitted to the platform. ::: :::field{name="likes" type="Int"} Number of likes/upvotes received. ::: :::field{name="views" type="Int"} Number of views the post has received. ::: :::field{name="comments" type="Int"} Number of comments on the post. ::: :::field{name="status" type="String"} Current status of the submission. ::: :: ### SocialSubmissionErrorType Represents an error that occurred during post submission. ::field-group :::field{required name="id" type="ID!"} Unique identifier for the submission error. ::: :::field{name="errorCode" type="String"} Platform-specific error code. ::: :::field{name="message" type="String"} Human-readable error message. ::: :::field{name="suggestedFix" type="String"} Suggested solution to resolve the error. ::: :::field{name="pendingSubmissionId" type="String"} ID of the original submission that failed. ::: :::field{name="dateFailed" type="DateTime"} When the submission failed. ::: :::field{name="dateAcknowledged" type="DateTime"} When the error was acknowledged by the user. ::: :: ### Timestamps All entities include standard timestamp fields: - `dateCreated` - When the record was first created - `dateUpdated` - When the record was last modified - `dateSubmitted` - When an action was performed (for submissions) ### Relationships Types are interconnected through relationships: - Users belong to Accounts - Users have multiple SocialAccounts - Media can be used across multiple posts - Submissions are linked to specific SocialAccounts ::tip When querying these types, use GraphQL fragments to reuse common field selections and keep your queries maintainable. :: ::note Some fields may be `null` depending on the platform or account type. Always handle potential null values in your client code. :: # Rate Limits Postpone enforces rate limits on API requests to ensure fair usage and maintain system stability for all users. This guide explains the rate limits, how they work, and best practices for handling them. ## Rate Limit Tiers Rate limits are applied per user and vary based on your account type: ::card-group :::card{icon="i-lucide-user" title="Free Accounts"} **25 requests** per 10 minutes ::: :::card{icon="i-lucide-crown" title="Paid Accounts"} **300 requests** per 10 minutes ::: :: The rate limit window is a sliding 10-minute period, meaning the limit resets continuously rather than at fixed intervals. ## Rate Limit Headers Every API response includes headers that provide information about your current rate limit status: | Header | Description | | ----------------------- | ----------------------------------------------------- | | `X-RateLimit-Limit` | Maximum number of requests allowed in the time window | | `X-RateLimit-Remaining` | Number of requests remaining in the current window | | `X-RateLimit-Reset` | Unix timestamp when the rate limit window resets | ### Example Response Headers ```http HTTP/1.1 200 OK X-RateLimit-Limit: 300 X-RateLimit-Remaining: 287 X-RateLimit-Reset: 1697472000 Content-Type: application/json ``` ## Rate Limit Exceeded When you exceed your rate limit, the API will return a `429 Too Many Requests` status code with an error response: ```json { "errors": [ { "message": "Rate limit exceeded. Please try again later.", "code": "rate_limit_exceeded" } ] } ``` ::warning When you receive a 429 response, you should wait until the time specified in the `X-RateLimit-Reset` header before making additional requests. :: ## Best Practices ### 1. Monitor Rate Limit Headers Always check the rate limit headers in your API responses to track your usage: ```javascript const response = await fetch('https://api.postpone.app/gql', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR-API-KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ query: '...' }) }); const remaining = response.headers.get('X-RateLimit-Remaining'); const resetTime = response.headers.get('X-RateLimit-Reset'); console.log(`Requests remaining: ${remaining}`); console.log(`Resets at: ${new Date(resetTime * 1000)}`); ``` ### 2. Implement Exponential Backoff When you receive a rate limit error, implement exponential backoff to avoid repeated failed requests: ```javascript async function makeRequestWithRetry(query, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { const response = await fetch('https://api.postpone.app/gql', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR-API-KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ query }) }); if (response.status === 429) { const resetTime = response.headers.get('X-RateLimit-Reset'); const waitTime = (resetTime * 1000) - Date.now(); if (i < maxRetries - 1) { await new Promise(resolve => setTimeout(resolve, waitTime)); continue; } } return response; } } ``` ### 3. Cache Responses Cache API responses when appropriate to reduce unnecessary requests: ```javascript const cache = new Map(); const CACHE_TTL = 5 * 60 * 1000; // 5 minutes async function getCachedData(query) { const cacheKey = JSON.stringify(query); const cached = cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.data; } const response = await makeAPIRequest(query); cache.set(cacheKey, { data: response, timestamp: Date.now() }); return response; } ``` ## Upgrading for Higher Limits If you're on a free account and need higher rate limits, consider upgrading to a paid plan. Paid accounts receive **12x more requests** (300 vs 25 per 10 minutes) Visit [postpone.app/plans](https://www.postpone.app/plans){rel=""nofollow""} to view our pricing and upgrade options. # AI & MCP Postpone ships a remote [Model Context Protocol (MCP)](https://modelcontextprotocol.io){rel=""nofollow""} server so AI agents can schedule and manage social media posts on your behalf using natural language. Connect it to clients like Claude, ChatGPT, Cursor, Perplexity, Grok, Zapier, n8n, Raycast, or any MCP-compatible tool. ::note The MCP server is in **Beta**. The tools and protocol versions it supports may change. :: ::tip Writing code against the Postpone API? The [Docs MCP server](https://developers.postpone.app/getting-started/docs-mcp) gives your AI coding assistant access to these docs, so it can look up queries, mutations, and fields as it writes your integration. :: ## MCP Server URL There's a single endpoint for the Postpone MCP server: ```text https://api.postpone.app/mcp ``` It speaks streamable HTTP (JSON-RPC over `POST`). Server-Sent Events (SSE) are not supported. The server negotiates MCP protocol versions `2025-11-25` and `2025-06-18`, preferring the newest the client supports. ::note A **confirmed email address** is required to use the MCP server. If your account email isn't confirmed yet, tool calls will fail with an `email_not_confirmed` error. :: ## Authentication The MCP endpoint accepts two authentication methods: **OAuth 2.1** (recommended for interactive clients) and **Bearer token** (a personal API key, ideal for headless or self-hosted clients). Session cookies are deliberately not honored. ### OAuth Most hosted AI clients (Claude, ChatGPT, Cursor, and others) support OAuth out of the box. When you add Postpone as a custom/remote MCP server and choose OAuth, the client walks you through signing in to Postpone and approving access — no API key to copy or paste. #### Scopes OAuth access is scoped. When you approve a connection, you grant a set of scopes, and each tool requires a specific one: | Scope | Grants | | --------------- | ------------------------------------------- | | `posts:read` | View your scheduled and published posts | | `posts:write` | Create and delete scheduled posts | | `accounts:read` | View your connected social accounts | | `media:read` | Search and view media in your media library | If a tool call needs a scope you haven't granted, the server responds with `403` and an `insufficient_scope` error so the client can request step-up authorization. ### Bearer token (API key) Any client that lets you set request headers can authenticate with a Postpone API key as a Bearer token. This is the simplest path for self-hosted clients (n8n, custom scripts, and similar). ::warning Unlike OAuth, **API key callers have full access** — scope checks are bypassed. Treat your API key as a secret that can read and write all of your Postpone content. Never share it publicly. :: #### Creating an MCP token API keys for MCP are the same personal API keys used for the GraphQL API: 1. Log into your Postpone account at [postpone.app](https://www.postpone.app){rel=""nofollow""}. 2. Go to [**Settings → Integrations → Postpone API**](https://www.postpone.app/settings/api){rel=""nofollow""}. You can also reach the MCP setup guides under [**Settings → AI Agents & MCP**](https://www.postpone.app/settings/mcp){rel=""nofollow""}. 3. **Create** an API key. Give it a name and, optionally, an expiration date. 4. **Copy** the key — this is the only time it's shown. API access requires a confirmed email and a plan that includes API access. For more on keys, see the [Authentication guide](https://developers.postpone.app/getting-started/authentication). #### Using the token Send the key in the `Authorization` header using the `Bearer` scheme. Most clients accept a JSON config like this — replace `API_KEY` with your key: ```json { "mcpServers": { "postpone": { "url": "https://api.postpone.app/mcp", "headers": { "Authorization": "Bearer API_KEY" } } } } ``` For Claude Code, add the server from your terminal: ```bash claude mcp add --transport http postpone https://api.postpone.app/mcp ``` ## Available Tools The Postpone MCP server exposes the following tools. Posting depends on IDs from other tools: get a `social_account_id` from `postpone_list_social_accounts` before creating a post, and `media_id` values from `postpone_search_media` before attaching content-library media. | Tool | Required scope | Description | | ------------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `postpone_list_social_accounts` | `accounts:read` | List the social media accounts connected to your Postpone account. Returns the `id` to pass as `social_account_id` when creating posts. | | `postpone_list_scheduled_posts` | `posts:read` | List posts scheduled within the next 30 days across all connected platforms, ordered by scheduled time. | | `postpone_list_published_posts` | `posts:read` | List posts published in the last 30 days across connected platforms, most recent first. | | `postpone_create_social_post` | `posts:write` | Schedule a post (or save a draft) on a platform. Supports text posts, threads, links, titles, subreddits, media by URL or content-library ID, and Instagram/Facebook media types. | | `postpone_delete_social_posts` | `posts:write` | Delete one or more scheduled posts. Cannot delete already-published posts. Results are reported per post. | | `postpone_search_media` | `media:read` | Search your content library for media (images, GIFs, and videos) and return IDs to attach to posts. | ::warning `postpone_create_social_post` and `postpone_delete_social_posts` change real content on your behalf. A well-behaved client will show you the exact content and ask for explicit confirmation before calling them. :: ## Rate Limits MCP tool calls share the same budget as the GraphQL API — **25 requests per 10 minutes** on free accounts and **300 requests per 10 minutes** on paid accounts. When you exceed the limit, the server returns a `429` with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. See the [Rate Limits guide](https://developers.postpone.app/getting-started/rate-limits) for details. ## Setup Guides Step-by-step instructions for individual clients live in your Postpone account under [**Settings → AI Agents & MCP**](https://www.postpone.app/settings/mcp){rel=""nofollow""}, with dedicated guides for Claude, ChatGPT, Perplexity, Grok, Zapier, n8n, Raycast, Cursor, and any other MCP client. # Docs MCP Server The Postpone API documentation has its own [Model Context Protocol (MCP)](https://modelcontextprotocol.io){rel=""nofollow""} server. Connect it to an AI coding assistant like Claude Code, Cursor, VS Code, or Windsurf, and the assistant can look up the exact queries, mutations, input fields, and examples it needs while it writes code against the Postpone API. ::note This is different from the [Postpone MCP server](https://developers.postpone.app/getting-started/ai-mcp). The **Docs MCP server** only reads these docs. It can't see your account or create posts, and it doesn't need an API key. The **Postpone MCP server** (`https://api.postpone.app/mcp`) acts on your Postpone account. Many developers connect both: the Docs MCP server to write integration code, and the Postpone MCP server to manage posts. :: ## MCP Server URL ```text https://developers.postpone.app/mcp ``` The server speaks streamable HTTP. It's public and read-only, so there's no authentication to set up. ## How It Works Once the server is connected, your assistant decides when to use it. For example, if you ask it to "write a Python script that schedules a Reddit post with an image," it can: 1. Call `list-pages` to find the relevant pages, such as [Reddit](https://developers.postpone.app/scheduling-posts/platforms/reddit) and [Uploading Media](https://developers.postpone.app/scheduling-posts/uploading-media). 2. Call `get-page` to read those pages in full. 3. Write code using the current field names, required inputs, and error handling from the docs instead of guessing. Because the assistant reads the live docs, it picks up new platforms and fields as soon as they're documented. Check the [API Changelog](https://developers.postpone.app/getting-started/changelog) to see what's new. ## Available Tools | Tool | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------- | | `list-pages` | Lists every documentation page with its title, path, description, and URL. Assistants use it to find relevant pages. | | `get-page` | Returns the full Markdown content of one page, given its path (for example, `/scheduling-posts/platforms/reddit`). | ## Setup ### Claude Code Add the server from your terminal: ```bash claude mcp add --transport http postpone-docs https://developers.postpone.app/mcp ``` ### Cursor :install-button{ide="cursor" label="Install in Cursor" url="https://developers.postpone.app/mcp"} Or add it manually to `.cursor/mcp.json` in your project root: ```json [.cursor/mcp.json] { "mcpServers": { "postpone-docs": { "type": "http", "url": "https://developers.postpone.app/mcp" } } } ``` ### Visual Studio Code Make sure the GitHub Copilot and GitHub Copilot Chat extensions are installed. :install-button{ide="vscode" label="Install in VS Code" url="https://developers.postpone.app/mcp"} Or add it manually to `.vscode/mcp.json`: ```json [.vscode/mcp.json] { "servers": { "postpone-docs": { "type": "http", "url": "https://developers.postpone.app/mcp" } } } ``` ### Windsurf 1. Open Windsurf and go to **Settings** > **Windsurf Settings** > **Cascade**. 2. Click **Manage MCPs**, then select **View raw config**. 3. Add the following configuration: ```json [.codeium/windsurf/mcp_config.json] { "mcpServers": { "postpone-docs": { "type": "http", "url": "https://developers.postpone.app/mcp" } } } ``` ### Other clients Any MCP client that supports streamable HTTP can connect. Add a server named `postpone-docs` with the URL `https://developers.postpone.app/mcp`. No headers are required. ## Other Ways to Give AI the Docs If your tool doesn't support MCP, you can still point it at the docs: - **llms.txt**: [llms.txt](https://developers.postpone.app/llms.txt){rel=""nofollow""} lists every page with a link to its Markdown version, and [llms-full.txt](https://developers.postpone.app/llms-full.txt){rel=""nofollow""} contains all of the docs in one file. - **Copy page**: every page has a **Copy page** button in the top right. It copies the page as Markdown, and its menu has options to open the page in ChatGPT or Claude. # API Changelog This changelog highlights notable additions and changes to the Postpone API. Minor documentation updates and internal changes are not included. ## November 2026 ### Docs MCP server The API documentation now has its own MCP server at `https://developers.postpone.app/mcp`. Connect it to Claude Code, Cursor, VS Code, or another AI coding assistant so it can look up queries, mutations, and fields while it writes code against the Postpone API. [Learn how to connect the Docs MCP server](https://developers.postpone.app/getting-started/docs-mcp) ## October 2026 ### Schedule Telegram posts The new `scheduleTelegramPost` and `updateScheduledTelegramPost` mutations schedule posts to connected Telegram channels and groups, with formatted text, photos, videos, albums, GIFs, link buttons, paid media, and options for link previews, silent sending, pinning, and content protection. `telegramPost` and `telegramSubmissions` retrieve them. [Learn how to schedule Telegram posts](https://developers.postpone.app/scheduling-posts/platforms/telegram) ### Set video cover images The new `setVideoCover` mutation sets the cover image of a Content Library video, either from a frame of the video (`offsetMs`) or from your own image (`imageUrl` or `imageMediaId`). Covers apply to Instagram Reels, TikTok videos, Facebook Reels, LinkedIn videos, and Pinterest video Pins. `removeVideoCover` removes a cover, and `MediaType.coverImage` returns it. [Learn how to set video covers](https://developers.postpone.app/scheduling-posts/video-covers) ## August 2026 ### Schedule Reddit posts using Account Schedules `scheduleRedditPost` now accepts `schedulingMode: QUEUE_NEXT`. Leave `postAt` off the submissions and the first one takes the next open slot in the Reddit account's Account Schedule, with the rest spaced 20 minutes apart behind it. [Learn about scheduling Reddit posts at the next open slot](https://developers.postpone.app/scheduling-posts/platforms/reddit#scheduling-at-the-next-open-slot) ### Query next scheduled dates for Reddit accounts `nextScheduleDates` now accepts Reddit accounts and returns a `scheduleDate` for them. [Learn about next scheduled dates](https://developers.postpone.app/scheduling-posts/next-scheduled-dates) ### Create drafts without a scheduled date Scheduling mutations can now create and update drafts without a `postAt` value on all supported platforms except Reddit. A date is still required before the draft can be published. [Learn about drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date) ### Schedule posts using Account Schedules Scheduling mutations now support `schedulingMode: QUEUE_NEXT`. Leave out `postAt` to place a post in the next open slot in the social account's Account Schedule. [Learn about scheduling at the next open slot](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot) ## July 2026 ### Attach media with a unified input Scheduling mutations now accept a `media` list. Each file can be referenced by Content Library ID, Content Library name, or public URL. This replaces the legacy `mediaUrl`, `mediaUrls`, and `mediaName` parameters, which remain available but are deprecated. [Learn about attaching media](https://developers.postpone.app/scheduling-posts#attaching-media) ### Upload media through the API New GraphQL mutations can add images, videos, and GIFs to the Content Library from a public URL or by uploading a local file directly. [Learn how to upload media](https://developers.postpone.app/scheduling-posts/uploading-media) ### Find fresh media for Reddit The Content Library `media` query now supports the `FRESH_FOR_SUBREDDIT` usage filter. Use it to find media that has not been submitted or scheduled in specified subreddits. [Learn about Content Library usage filters](https://developers.postpone.app/queries/content-library#mediausagefilterinput) ## June 2026 ### Connect AI agents with MCP The Postpone Model Context Protocol (MCP) server launched in beta. MCP-compatible clients can list accounts and posts, search media, and create or delete scheduled posts using OAuth or an API key. [Learn how to connect with MCP](https://developers.postpone.app/getting-started/ai-mcp) ## May 2026 ### Schedule Threads topic tags and ghost posts The Threads scheduling mutation now supports explicit topic tags and text-only ghost posts that automatically archive after 24 hours. [Learn about scheduling Threads posts](https://developers.postpone.app/scheduling-posts/platforms/threads) # User The `profile` query returns information about the currently authenticated user, including their account details and plan. ## UserType ::field-group :::field{name="id" type="ID!"} Unique identifier for the user. ::: :::field{name="email" type="String"} The user's email address. ::: :::field{name="username" type="String"} The user's username. ::: :::field{name="fullName" type="String"} The user's full display name. ::: :::field{name="timezone" type="String"} The user's timezone (e.g. `America/New_York`). ::: :::field{name="registeredAt" type="DateTime"} When the user registered their account. ::: :::field{name="account" type="AccountType"} The account this user belongs to. See [AccountType](https://developers.postpone.app/#accounttype) below. ::: :::field --- name: socialAccounts type: "[SocialAccountType]" --- Social media accounts connected to this user's account. See [Social Accounts](https://developers.postpone.app/queries/social-accounts) for the full type reference. ::: :: ## AccountType ::field-group :::field{name="id" type="ID!"} Unique identifier for the account. ::: :::field{name="name" type="String"} The account's display name. ::: :::field{name="plan" type="PlanType"} The account's current subscription plan. See [PlanType](https://developers.postpone.app/#plantype) below. ::: :::field{name="subscriptionStatus" type="String"} Current subscription status (e.g. `active`, `canceled`, `trialing`). ::: :::field{name="postsSubmittedThisMonth" type="Int"} Number of posts submitted in the current billing month. ::: :: ## PlanType ::field-group :::field{name="code" type="String"} The plan's unique code identifier (e.g. `creator`, `professional`). ::: :::field{name="label" type="String"} Human-readable plan name (e.g. `Creator`, `Professional`). ::: :::field{name="allowedMonthlyPosts" type="Int"} Maximum posts per month allowed by this plan. ::: :::field{name="allowedMonthlyTweets" type="Int"} Maximum tweets per month allowed by this plan. ::: :: ## profile Query Retrieve the authenticated user's profile. No arguments are required. ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Query"} ```graphql query profile { profile { id email username fullName timezone registeredAt remainingMonthlyPosts account { id name subscriptionStatus allowedMonthlyPosts postsSubmittedThisMonth plan { code label allowedMonthlyPosts } } } } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "profile": { "id": "123", "email": "user@example.com", "username": "myusername", "fullName": "Jane Doe", "timezone": "America/New_York", "registeredAt": "2023-01-15T10:30:00Z", "remainingMonthlyPosts": 185, "account": { "id": "456", "name": "My Account", "subscriptionStatus": "active", "allowedMonthlyPosts": 200, "postsSubmittedThisMonth": 15, "plan": { "code": "creator", "label": "Creator", "allowedMonthlyPosts": 200, "hasApiAccess": true, "hasMultiUser": false } } } } } ``` :::: ::: :: # Social Accounts The `socialAccounts` query returns the social media accounts connected to your Postpone account. ## SocialAccountType ::field-group :::field{name="id" type="ID!"} Unique identifier for the social account. ::: :::field{name="username" type="String"} The platform username for this account. ::: :::field{name="formattedUsername" type="String"} A display-formatted version of the username (e.g. `u/myuser` for Reddit, `@myuser` for Twitter). ::: :::field{name="name" type="String"} The display name associated with this account. ::: :::field{name="platform" type="String"} The social media platform (e.g. `reddit`, `twitter`, `instagram`). ::: :::field{name="avatarUrl" type="String"} URL to the account's profile avatar image. ::: :::field{name="profileUrl" type="String"} URL to the account's public profile page. ::: :::field{name="description" type="String"} Bio or description text from the platform profile. ::: :::field{name="followers" type="Int"} Number of followers or subscribers. ::: :::field{name="isConnected" type="Boolean"} Whether Postpone currently has a valid connection to this account. ::: :::field{name="isEnabled" type="Boolean"} Whether this account is enabled for scheduling. ::: :::field{name="verified" type="Boolean"} Whether the account is verified on the platform. ::: :::field{name="dateCreated" type="DateTime"} When this social account was connected to Postpone. ::: :: ## socialAccounts Query Returns a list of connected social accounts, optionally filtered by platform. ::field-group :::field{name="platform" type="String"} Optional. Filter results to a specific platform. Accepted values: `reddit`, `twitter`, `instagram`, `facebook`, `threads`, `bluesky`, `tiktok`, `youtube`, `linkedin`, `pinterest`, `mastodon`, `tumblr`, `telegram`. ::: :: ### List All Connected Accounts ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Query"} ```graphql query socialAccounts { socialAccounts { id username formattedUsername name platform followers avatarUrl profileUrl isConnected isEnabled verified } } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "socialAccounts": [ { "id": "789", "username": "myreddituser", "formattedUsername": "u/myreddituser", "name": "My Reddit Account", "platform": "reddit", "followers": 1250, "avatarUrl": "https://example.com/avatar.png", "profileUrl": "https://reddit.com/u/myreddituser", "isConnected": true, "isEnabled": true, "verified": false }, { "id": "790", "username": "mytwitter", "formattedUsername": "@mytwitter", "name": "My Twitter Account", "platform": "twitter", "followers": 3400, "avatarUrl": "https://example.com/twitter-avatar.png", "profileUrl": "https://twitter.com/mytwitter", "isConnected": true, "isEnabled": true, "verified": false } ] } } ``` :::: ::: :: ### Filter by Platform ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Query"} ```graphql query socialAccounts($platform: String) { socialAccounts(platform: $platform) { id username formattedUsername name platform followers isConnected isEnabled } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "platform": "instagram" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "socialAccounts": [ { "id": "791", "username": "myinstaaccount", "formattedUsername": "@myinstaaccount", "name": "My Instagram", "platform": "instagram", "followers": 8200, "isConnected": true, "isEnabled": true } ] } } ``` :::: ::: :: # Content Library The `media` query retrieves files from your Content Library with support for filtering, searching, and pagination. ## MediaType ::field-group :::field{name="id" type="ID!"} Unique identifier for the media file. ::: :::field{name="name" type="String"} The filename as stored in your Content Library. ::: :::field{name="url" type="String"} Direct URL to the media file. ::: :::field{name="thumbnailUrl" type="String"} URL to a thumbnail preview of the media. ::: :::field{name="mimeType" type="String"} The MIME type of the file (e.g. `image/jpeg`, `video/mp4`). ::: :::field{name="size" type="BigInt"} File size in bytes. ::: :::field{name="humanReadableSize" type="String"} Human-readable file size (e.g. `2.4 MB`). ::: :::field{name="isImage" type="Boolean"} Whether this file is an image. ::: :::field{name="isVideo" type="Boolean"} Whether this file is a video. ::: :::field{name="isGif" type="Boolean"} Whether this file is a GIF. ::: :::field{name="duration" type="Int"} Duration of a video in milliseconds. `0` for images, GIFs, and videos whose duration is unknown. ::: :::field{name="coverImage" type="CoverImageType"} The video's cover image, or `null` when it has none. See [CoverImageType](https://developers.postpone.app/#coverimagetype) below and [Video Covers](https://developers.postpone.app/scheduling-posts/video-covers). ::: :::field{name="altText" type="String"} Accessibility alt text for the media. ::: :::field{name="dateCreated" type="DateTime"} When the file was added to the Content Library. ::: :::field{name="dateUpdated" type="DateTime"} When the file was last updated. ::: :::field --- name: labels type: "[MediaLabelType]" --- Labels assigned to this media file. See [MediaLabelType](https://developers.postpone.app/#medialabeltype) below. ::: :: ## CoverImageType ::field-group :::field{name="url" type="String"} URL of the cover image. For covers chosen from a frame of the video, this is the frame captured at `offsetMs`. ::: :::field{name="offsetMs" type="Int"} Position of the cover frame in the video, in milliseconds. `null` when the cover is a custom image. ::: :: ## MediaLabelType ::field-group :::field{name="id" type="ID!"} Unique identifier for the label. ::: :::field{name="text" type="String"} The label text. ::: :::field{name="color" type="String"} The label color as a hex string (e.g. `#FF5733`). ::: :: ## media Query Returns a paginated list of media from your Content Library. ::field-group :::field{name="orderBy" type="String"} Sort order. Prefix with `-` for descending. Supported values: `id`, `name`, `size`, `mimeType`, `dateCreated`. Default: `-id`. ::: :::field{name="page" type="Int"} Page number for pagination. Default: `1`. ::: :::field{name="limit" type="Int"} Number of results per page. Maximum: `100`. Default: `25`. ::: :::field{name="search" type="String"} Filter by filename (case-insensitive substring match). ::: :::field{name="fileType" type="FileType"} Filter by file type. Values: `IMAGE`, `VIDEO`, `GIF`. ::: :::field{name="usageFilter" type="MediaUsageFilterInput"} Filter by how media has been used across platforms. See [MediaUsageFilterInput](https://developers.postpone.app/#mediausagefilterinput) below. ::: :: ## MediaUsageFilterInput ::field-group :::field{required name="filterType" type="MediaUsageFilterType!"} The type of usage filter to apply. - `UNUSED` — Media that has not been scheduled or submitted - `UNSCHEDULED` — Media not currently scheduled for any future post - `UNSUBMITTED` — Media that has never been submitted to a post - `FRESH_FOR_SUBREDDIT` — Reddit only. Media that has never been submitted to the given `subreddits` and is not currently scheduled in any subreddit. Requires `subreddits`, and `platforms` must be omitted or `["REDDIT"]` ::: :::field --- name: platforms type: "[String]" --- Limit the filter to specific platforms (e.g. `["REDDIT", "TWITTER"]`). Defaults to all connected platforms if omitted (or `["REDDIT"]` for `FRESH_FOR_SUBREDDIT`). ::: :::field --- name: subreddits type: "[String]" --- Filter by specific Reddit subreddits. Only valid when `REDDIT` is included in `platforms`. Required for `FRESH_FOR_SUBREDDIT`. ::: :: ### List Media ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Query"} ```graphql query media( $orderBy: String $limit: Int $page: Int $search: String $fileType: FileType ) { media( orderBy: $orderBy limit: $limit page: $page search: $search fileType: $fileType ) { total objects { id name mimeType size humanReadableSize url thumbnailUrl dateCreated labels { id text color } } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "limit": 20, "page": 1, "orderBy": "-date_created", "fileType": "IMAGE" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "media": { "total": 45, "objects": [ { "id": "101", "name": "meme-2024.jpg", "mimeType": "image/jpeg", "size": 1048576, "humanReadableSize": "1 MB", "url": "https://media.postpone.app/meme-2024.jpg", "thumbnailUrl": "https://media.postpone.app/thumb/meme-2024.jpg", "dateCreated": "2025-01-15T14:30:00Z", "labels": [ { "id": "1", "text": "Memes", "color": "#FF5733" } ] } ] } } } ``` :::: ::: :: ### Filter by Usage Filter media based on whether it has been scheduled or submitted across platforms. ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Query"} ```graphql query media( $limit: Int $page: Int $orderBy: String $usageFilter: MediaUsageFilterInput ) { media( limit: $limit page: $page orderBy: $orderBy usageFilter: $usageFilter ) { total objects { id name url thumbnailUrl dateCreated } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables - Unsubmitted"} Find media never submitted to Twitter or Instagram: ```json { "limit": 20, "page": 1, "orderBy": "-date_created", "usageFilter": { "filterType": "UNSUBMITTED", "platforms": ["TWITTER", "INSTAGRAM"] } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables - Unscheduled"} Find media not scheduled in specific subreddits: ```json { "limit": 20, "page": 1, "orderBy": "-date_created", "usageFilter": { "filterType": "UNSCHEDULED", "platforms": ["REDDIT"], "subreddits": ["funny", "pics"] } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables - Fresh for Subreddit"} Find media never submitted to a specific subreddit and not currently scheduled in any subreddit: ```json { "limit": 20, "page": 1, "orderBy": "-date_created", "usageFilter": { "filterType": "FRESH_FOR_SUBREDDIT", "subreddits": ["funny"] } } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "media": { "total": 15, "objects": [ { "id": "201", "name": "unused-image.jpg", "url": "https://media.postpone.app/unused-image.jpg", "thumbnailUrl": "https://media.postpone.app/thumb/unused-image.jpg", "dateCreated": "2025-01-15T14:30:00Z" } ] } } } ``` :::: ::: :: # Introduction ## Scheduling Posts via API The Postpone API provides powerful GraphQL mutations to schedule posts across multiple social media platforms. This comprehensive guide will walk you through the process of scheduling posts programmatically. ::tip Before getting started with scheduling posts, make sure you've read through the [API introduction](https://developers.postpone.app/getting-started) to learn how to work with the Postpone API playground and make API calls. :: ## Overview Postpone supports scheduling posts to the following platforms through dedicated GraphQL mutations: ::card-group :::card --- ui: icon: text-[#ff4500] icon: i-simple-icons-reddit title: Reddit to: https://developers.postpone.app/scheduling-posts/platforms/reddit --- Schedule notification posts to multiple subreddits with custom timing, flair selection, and more. ::: :::card --- ui: icon: text-[#1a1a18] dark:text-white icon: i-simple-icons-x title: X/Twitter to: https://developers.postpone.app/scheduling-posts/platforms/twitter --- Schedule tweets with media attachments and thread support. ::: :::card --- ui: icon: text-[#e1306c] icon: i-simple-icons-instagram title: Instagram to: https://developers.postpone.app/scheduling-posts/platforms/instagram --- Schedule photo and video posts with captions and hashtags. ::: :::card --- ui: icon: text-[#1877f2] icon: i-simple-icons-facebook title: Facebook to: https://developers.postpone.app/scheduling-posts/platforms/facebook --- Schedule posts to Facebook Pages. ::: :::card --- ui: icon: text-[#000000] dark:text-white icon: i-simple-icons-threads title: Threads to: https://developers.postpone.app/scheduling-posts/platforms/threads --- Schedule posts to Meta's Threads platform. ::: :::card --- ui: icon: text-[#0085ff] icon: i-simple-icons-bluesky title: Bluesky to: https://developers.postpone.app/scheduling-posts/platforms/bluesky --- Schedule posts to the decentralized Bluesky network. ::: :::card --- ui: icon: text-[#000000] dark:text-white icon: i-simple-icons-tiktok title: TikTok to: https://developers.postpone.app/scheduling-posts/platforms/tiktok --- Schedule video content to TikTok. ::: :::card --- ui: icon: text-[#ff0000] icon: i-simple-icons-youtube title: YouTube Shorts to: https://developers.postpone.app/scheduling-posts/platforms/youtube --- Schedule short-form video content to YouTube. ::: :::card --- ui: icon: text-[#0077b5] icon: i-simple-icons-linkedin title: LinkedIn to: https://developers.postpone.app/scheduling-posts/platforms/linkedin --- Schedule professional posts to LinkedIn profiles and pages. ::: :::card --- ui: icon: text-[#e60023] icon: i-simple-icons-pinterest title: Pinterest to: https://developers.postpone.app/scheduling-posts/platforms/pinterest --- Schedule pins to Pinterest boards. ::: :::card --- ui: icon: text-[#6364ff] icon: i-simple-icons-mastodon title: Mastodon to: https://developers.postpone.app/scheduling-posts/platforms/mastodon --- Schedule posts to Mastodon instances. ::: :::card --- ui: icon: text-[#001935] dark:text-white icon: i-simple-icons-tumblr title: Tumblr to: https://developers.postpone.app/scheduling-posts/platforms/tumblr --- Schedule posts to Tumblr blogs. ::: :::card --- ui: icon: text-[#26a5e4] icon: i-simple-icons-telegram title: Telegram to: https://developers.postpone.app/scheduling-posts/platforms/telegram --- Schedule posts to Telegram channels and groups. ::: :: ## Platform-Specific Mutations Each platform has its own dedicated GraphQL mutation for scheduling posts. Here's a quick reference: | Platform | Mutation Name | Description | | ------------------------------------------------------------------------------------ | ----------------------- | ---------------------------------------------- | | [Reddit](https://developers.postpone.app/scheduling-posts/platforms/reddit) | `scheduleRedditPost` | Schedule posts to multiple subreddits | | [X/Twitter](https://developers.postpone.app/scheduling-posts/platforms/twitter) | `scheduleTweet` | Schedule tweets and threads | | [Instagram](https://developers.postpone.app/scheduling-posts/platforms/instagram) | `scheduleInstagramPost` | Schedule feed posts, Reels, and Stories posts | | [Facebook](https://developers.postpone.app/scheduling-posts/platforms/facebook) | `scheduleFacebookPost` | Schedule posts to Pages | | [Threads](https://developers.postpone.app/scheduling-posts/platforms/threads) | `scheduleThreadsPost` | Schedule posts to Threads | | [Bluesky](https://developers.postpone.app/scheduling-posts/platforms/bluesky) | `scheduleBlueskyPost` | Schedule posts to Bluesky | | [TikTok](https://developers.postpone.app/scheduling-posts/platforms/tiktok) | `scheduleTikTokPost` | Schedule video content | | [YouTube Shorts](https://developers.postpone.app/scheduling-posts/platforms/youtube) | `scheduleYouTubePost` | Schedule short-form videos | | [LinkedIn](https://developers.postpone.app/scheduling-posts/platforms/linkedin) | `scheduleLinkedInPost` | Schedule professional posts | | [Pinterest](https://developers.postpone.app/scheduling-posts/platforms/pinterest) | `schedulePinterestPost` | Schedule pins to boards | | [Mastodon](https://developers.postpone.app/scheduling-posts/platforms/mastodon) | `scheduleMastodonPost` | Schedule posts to Mastodon | | [Tumblr](https://developers.postpone.app/scheduling-posts/platforms/tumblr) | `scheduleTumblrPost` | Schedule posts to Tumblr | | [Telegram](https://developers.postpone.app/scheduling-posts/platforms/telegram) | `scheduleTelegramPost` | Schedule posts to Telegram channels and groups | ## Common Parameters While each platform has specific requirements, most scheduling mutations share common parameters: ::field-group :::field{required name="username" type="String!"} The username of the connected social media account to post from. ::: :::field{name="postAt" type="DateTime"} ISO 8601 timestamp indicating when the post should be published (must be in the future). Required, except on a draft (see [Drafts without a date](https://developers.postpone.app/#drafts-without-a-date)) or on a new post scheduled with `schedulingMode: QUEUE_NEXT`. ::: :::field{name="schedulingMode" type="SchedulingModeType"} How the post date is decided. Options: `SPECIFIC_TIME`, `QUEUE_NEXT` (default: `SPECIFIC_TIME`). Only accepted when scheduling a new post. See [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ::: :::field{name="text" type="String"} The main text content of your post (character limits vary by platform). ::: :::field --- name: media type: "[SimpleMediaInput]" --- Media files to attach to the post, each referenced by `id`, `name`, or `url`. See [Attaching Media](https://developers.postpone.app/#attaching-media). ::: :::field{name="gallery" type="GalleryInput"} Media gallery for platforms that support per-item captions and outbound links. Only needed when `media` isn't enough. ::: :::field{name="publishingStatus" type="PublishingStatusType"} Publishing status. Options: `READY_TO_PUBLISH`, `DRAFT` (default: `READY_TO_PUBLISH`). ::: :::field --- name: postTags type: "[ID!]" --- Array of post tag IDs to organize your content. ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates). ::: :: ## Scheduling at the Next Open Slot Instead of working out a time yourself, you can hand that job to Postpone: pass `schedulingMode: QUEUE_NEXT`, leave `postAt` out, and the post lands on the next open slot in the account's [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""}. ```json { "input": { "username": "myhandle", "schedulingMode": "QUEUE_NEXT" } } ``` The rest of the input stays exactly as it was — only the date changes hands. Each platform page has a worked example. A slot counts as taken when a scheduled post falls on it, or within about 30 minutes of it. Queue several posts in a row and they walk down the schedule one open slot at a time rather than piling onto the same time. The queue never fills up either: an account booked solid for months gets the first slot past the end of its queue. This is the single-request version of querying [Next Scheduled Dates](https://developers.postpone.app/scheduling-posts/next-scheduled-dates) and passing the result back as `postAt`. Prefer it over that two-step approach when you are only placing a post — because the slot is picked and taken in one transaction, two posts queued at the same moment can never be handed the same slot. ### Rules - **A schedule is required.** The account has to have an Account Schedule set up. Without one the mutation fails with ` has no schedule set up.` - **`postAt` and `QUEUE_NEXT` are mutually exclusive.** Pass one or the other, never both. Sending both fails with `Pass either a post date or QUEUE_NEXT, not both.` - **New posts only.** Update mutations reject `schedulingMode` outright — `QUEUE_NEXT` and `SPECIFIC_TIME` alike — with `schedulingMode is not supported when updating a post.` An already-scheduled post occupies a slot, so queueing it again would read that slot as taken and walk the post forward. To move a scheduled post, pass a `postAt`. - **Not for drafts.** A draft doesn't hold a slot, so queueing one would hand the same slot to every other queued post. Save the draft without a date (or with a `postAt`), then set a date when you promote it to `publishingStatus: READY_TO_PUBLISH`. - **Reddit queues one slot, not one per subreddit.** A Reddit post's submissions are scheduled independently, so `QUEUE_NEXT` takes a slot for the first submission you list and spaces the rest 20 minutes apart behind it. See [Reddit](https://developers.postpone.app/scheduling-posts/platforms/reddit#scheduling-at-the-next-open-slot). ::note On platforms where `postAt` lives inside `submissions` (Reddit, Instagram, Facebook, TikTok, YouTube Shorts, LinkedIn, Pinterest, and Tumblr), `schedulingMode` sits at the top level of the input instead. On all of them but Reddit it decides one date for the whole post, and every submission shares that date. Reddit is the exception: its submissions each get a date of their own. :: ::note `QUEUE_NEXT` always reads the **Account Schedule**. On Reddit that is the only schedule it reads — the per-subreddit schedules you set up in the Subreddit Manager are used by the post form in the Postpone app, not by the API. :: ## Drafts without a date A draft is somewhere to put a post before you've decided when it goes out, so `postAt` is optional on one: ```json { "input": { "username": "myhandle", "text": "still thinking about this one", "publishingStatus": "DRAFT" } } ``` A date is still stored against the post, but it's a placeholder parked about 30 days out, not a time you chose. ### Rules - **Drafts only.** A post going out as `READY_TO_PUBLISH` still has to say when. Leaving `postAt` off one fails with `Provide a post date, or use QUEUE_NEXT.` - **Promotion needs a date.** Promoting a draft to `publishingStatus: READY_TO_PUBLISH` without a `postAt` fails with `Provide a post date.` `schedulingMode` isn't accepted on an update either, so the date has to be spelled out. - **Editing doesn't need one.** Updating a draft that stays a draft can leave `postAt` out; the draft stays parked. Pass a `postAt` to give it a real date at any point. - **A lapsed date gets re-parked.** If a draft's stored date has passed by the time you edit it, a dateless edit pushes the placeholder back out. A date still in the future is never moved by an edit, whether you chose it or we parked it there. - **Reddit is not supported.** `scheduleRedditPost` requires a `postAt` on each submission, drafts included, unless the post is queued with [`schedulingMode: QUEUE_NEXT`](https://developers.postpone.app/#scheduling-at-the-next-open-slot) — and a draft can't be queued. ::note On platforms where `postAt` lives inside `submissions`, just leave it off the submission entries. `publishingStatus` sits at the top level of the input either way. :: ## Attaching Media Scheduling mutations take a `media` parameter: a list of the images, videos, or GIFs to attach to the post. Each item points at a single file, in one of three ways: ::field-group :::field{name="id" type="ID"} ID of a file already in your Content Library. ::: :::field{name="name" type="String"} Name of a file in your Content Library. The first match is used, and the match is case-insensitive. ::: :::field{name="url" type="String"} Public URL of a file to be uploaded to your Content Library. ::: :: Every item must set **exactly one** of `id`, `name`, or `url`, and no file can be attached to the same post twice. You can mix the three forms freely within one list: ```json { "media": [ { "id": "1001" }, { "name": "company-logo.png" }, { "url": "https://example.com/images/hero.jpg" } ] } ``` If a file can't be resolved, the whole mutation fails with an error rather than scheduling the post without it. The number of items you can attach depends on the platform, so check the platform-specific docs. See [Uploading Media](https://developers.postpone.app/scheduling-posts/uploading-media) for how to get files into your Content Library and pick up their `id`. Videos can also have a cover image, which Instagram, TikTok, Facebook, LinkedIn, and Pinterest show before the video plays. See [Video Covers](https://developers.postpone.app/scheduling-posts/video-covers). ::note `media` is only supported when scheduling a new post. Update mutations reject it. :: ### Deprecated Media Parameters `mediaUrl`, `mediaUrls`, and `mediaName` are deprecated in favor of `media`. They still work, but new integrations should use `media`, and existing ones should migrate: | Deprecated | Use instead | | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `"mediaName": "logo.png"` | `"media": [{ "name": "logo.png" }]` | | `"mediaUrl": "https://example.com/a.jpg"` | `"media": [{ "url": "https://example.com/a.jpg" }]` | | `"mediaUrls": ["https://example.com/a.jpg", "https://example.com/b.jpg"]` | `"media": [{ "url": "https://example.com/a.jpg" }, { "url": "https://example.com/b.jpg" }]` | ## Response Format All scheduling mutations return a consistent response structure: ```graphql { success: Boolean! errors: [ValidationError] } ``` ::field-group :::field{name="success" type="boolean"} Indicates whether the post was successfully scheduled. ::: :::field --- name: errors type: "[ValidationError]" --- Array of validation errors with specific error codes and messages. ::: :: ## Common Errors These errors are commonly encountered across multiple platforms when scheduling posts: ::accordion :::accordion-item{icon="i-lucide-alert-circle" label="Account not connected"} The specified username is not connected to your Postpone account. Connect the social media account in your settings first. ::: :::accordion-item --- icon: i-lucide-calendar-x label: Post date cannot be in the past --- The `postAt` timestamp must be in the future. Check your timezone settings and ensure the date is correct. ::: :::accordion-item --- icon: i-lucide-calendar-off label: Provide a post date, or use QUEUE_NEXT --- A new post going out has to say when: pass a `postAt`, or pass `schedulingMode: QUEUE_NEXT` to take the next slot on the account's schedule. Drafts are exempt — see [Drafts without a date](https://developers.postpone.app/#drafts-without-a-date). ::: :::accordion-item{icon="i-lucide-calendar-off" label="Provide a post date"} `schedulingMode` isn't accepted on updates, so a `postAt` is the only way to say when the post goes out. You'll also see it when promoting a parked draft to `publishingStatus: READY_TO_PUBLISH` without giving it a date. ::: :::accordion-item --- icon: i-lucide-git-fork label: Pass either a post date or QUEUE_NEXT, not both --- `postAt` and `schedulingMode: QUEUE_NEXT` are mutually exclusive. Drop the `postAt` to queue the post, or drop the `schedulingMode` to keep your own time. ::: :::accordion-item --- icon: i-lucide-calendar-clock label: Account has no schedule set up --- `schedulingMode: QUEUE_NEXT` needs an [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""} on the account you're posting to. Set one up, or pass an explicit `postAt`. ::: :::accordion-item --- icon: i-lucide-pencil-off label: schedulingMode is not supported when updating a post --- `schedulingMode` is only accepted when scheduling a new post. To move a post that is already scheduled, pass a `postAt` instead. ::: :::accordion-item --- icon: i-lucide-file-clock label: Drafts cannot use QUEUE_NEXT --- Drafts don't hold a slot in the queue. Save the draft without a date, then pass a `postAt` when you update it to `publishingStatus: READY_TO_PUBLISH`. ::: :::accordion-item{icon="i-lucide-type" label="Content exceeds character limit"} Your post content exceeds the platform's character limit. Each platform has different limits - check the platform-specific documentation for details. ::: :::accordion-item --- icon: i-lucide-trending-up label: Over plan limits for scheduled month --- You've reached your plan's posting limit for the scheduled month. Upgrade your plan or schedule for a different month. ::: :::accordion-item{icon="i-lucide-file-x" label="Media file too large"} The media file exceeds the platform's size limits. Each platform has different file size restrictions - optimize your media or check platform limits. ::: :::accordion-item{icon="i-lucide-image-off" label="Invalid media format"} The media format is not supported by the target platform. Check the platform's supported file types and convert if necessary. ::: :::accordion-item --- icon: i-lucide-unlink label: Cannot combine media with other content types --- Some platforms don't allow combining certain content types (e.g., polls with media, text with links). Choose one content type per post. ::: :: ## Best Practices When using the scheduling API, keep these best practices in mind: ::accordion :::accordion-item{icon="i-lucide-timer" label="Respect Rate Limits"} Each platform has its own rate limits. Space out your API calls appropriately to avoid hitting these limits. ::: :::accordion-item{icon="i-lucide-calendar-check" label="Validate Timestamps"} Ensure your `postAt` timestamps are in the future and properly formatted in ISO 8601 format (e.g., `2025-09-16T14:30:00.000Z`). If you're only picking a time to spread posts out, let Postpone pick it for you with [`schedulingMode: QUEUE_NEXT`](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ::: :::accordion-item --- icon: i-lucide-alert-triangle label: Handle Errors Gracefully --- Always check the response for errors and validation issues. Implement proper error handling in your application. ::: :::accordion-item{icon="i-lucide-play" label="Test with Playground"} Use the API playground to test your mutations before implementing them in your application. ::: :: ## Getting Started To begin scheduling posts via the API: ::steps{level="3"} ### Connect Your Accounts Ensure you have connected the social media accounts you want to post to in your Postpone dashboard. ### Obtain API Credentials Get your API key from Postpone settings to authenticate your requests. ### Choose Your Platform Select the platform you want to schedule posts for and refer to the platform-specific documentation. ### Test Your Integration Use the API playground to test your mutations before implementing them in production. :: ## Next Steps Ready to start scheduling posts? Choose a platform from the list above to see detailed examples and platform-specific requirements. ::note Each platform has unique features and requirements. Be sure to read the platform-specific documentation for detailed parameter information and examples. :: # Managing Posts Here are common GraphQL mutations you can execute against the Postpone API to create, update, and manage your social media posts across different platforms. ## Scheduling Posts To schedule posts for specific platforms, check out the [platform-specific post scheduling documentation](https://developers.postpone.app/scheduling-posts/platforms). ## Submit Platform Post Immediately submit a scheduled post to the platform. This mutation publishes a post that was previously scheduled, sending it to the platform right away. ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation submitPlatformPost($platform: SocialPlatform!, $postId: ID!) { submitPlatformPost(platform: $platform, postId: $postId) { success errors { field code message } submissionError { errorCode message suggestedFix } post { id platform submissions { id url } } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "platform": "TWITTER", "postId": "12345" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "submitPlatformPost": { "success": true, "errors": [], "submissionError": null, "post": { "id": "12345", "platform": "twitter", "submissions": [ { "id": "67890", "url": "https://twitter.com/user/status/123456789" } ] } } } } ``` :::: ::: :: ::field-group :::field{required name="platform" type="SocialPlatform"} The social media platform. Valid values: `TWITTER`, `INSTAGRAM`, `FACEBOOK`, `THREADS`, `BLUESKY`, `TIKTOK`, `YOUTUBE`, `LINKEDIN`, `PINTEREST`, `MASTODON`, `TUMBLR`, `TELEGRAM`. Note: Reddit is not supported via this mutation. ::: :::field{required name="postId" type="ID"} The unique identifier of the post to submit. ::: :: ## Reschedule Platform Post Change the scheduled time for a post. This mutation updates when a post will be published to the platform. ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation reschedulePlatformPost( $platform: SocialPlatform! $postId: ID! $postAt: DateTime! ) { reschedulePlatformPost( platform: $platform postId: $postId postAt: $postAt ) { success errors { field code message } post { id platform submissions { id postAt } } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "platform": "TWITTER", "postId": "12345", "postAt": "2025-02-01T14:30:00Z" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "reschedulePlatformPost": { "success": true, "errors": [], "post": { "id": "12345", "platform": "twitter", "submissions": [ { "id": "67890", "postAt": "2025-02-01T14:30:00Z" } ] } } } } ``` :::: ::: :: ::field-group :::field{required name="platform" type="SocialPlatform"} The social media platform. Valid values: `TWITTER`, `INSTAGRAM`, `FACEBOOK`, `THREADS`, `BLUESKY`, `TIKTOK`, `YOUTUBE`, `LINKEDIN`, `PINTEREST`, `MASTODON`, `TUMBLR`, `TELEGRAM`. Note: Reddit is not supported via this mutation. ::: :::field{required name="postId" type="ID"} The unique identifier of the post to reschedule. ::: :::field{required name="postAt" type="DateTime"} The new scheduled date and time for the post in ISO 8601 format. Must be in the future. ::: :: ## Schedule Platform Submission Schedule a specific submission for publishing at a given time. This is useful for scheduling individual submissions within a multi-platform post. ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation schedulePlatformSubmission( $platform: SocialPlatform! $submissionId: ID! $postAt: DateTime! ) { schedulePlatformSubmission( platform: $platform submissionId: $submissionId postAt: $postAt ) { success errors { field code message } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "platform": "INSTAGRAM", "submissionId": "67890", "postAt": "2025-02-01T16:00:00Z" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "schedulePlatformSubmission": { "success": true, "errors": [] } } } ``` :::: ::: :: ::field-group :::field{required name="platform" type="SocialPlatform"} The social media platform for this submission. ::: :::field{required name="submissionId" type="ID"} The unique identifier of the submission to schedule. ::: :::field{required name="postAt" type="DateTime"} The scheduled date and time for the submission in ISO 8601 format. Must be in the future. ::: :: ::note If the submission is in the trash, it will be automatically restored when scheduled. If the post is a draft, it will be converted to ready-to-publish status. :: ## Remove Submission From Platform Delete a published post from the social media platform. This removes the content from the platform itself (not just from Postpone). ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation removeSubmissionFromPlatform( $platform: SocialPlatform! $submissionId: ID! ) { removeSubmissionFromPlatform( platform: $platform submissionId: $submissionId ) { success errors { field code message } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "platform": "TWITTER", "submissionId": "67890" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "removeSubmissionFromPlatform": { "success": true, "errors": [] } } } ``` :::: ::: :: ::field-group :::field{required name="platform" type="SocialPlatform"} The social media platform where the submission was published. Supported platforms: `THREADS`, `TWITTER`, `YOUTUBE`, `BLUESKY`, `MASTODON`, `TELEGRAM`. ::: :::field{required name="submissionId" type="ID"} The unique identifier of the submission to remove from the platform. ::: :: ::warning This action permanently deletes the post from the social media platform. The submission must have already been published to the platform. Only supported on Threads, Twitter, YouTube, Bluesky, Mastodon, and Telegram. :: ## Delete Platform Post Move a post to the trash or permanently delete it from Postpone. By default, this performs a soft delete (moves to trash). ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation deletePlatformPost( $platform: SocialPlatform! $postId: ID! $hardDelete: Boolean ) { deletePlatformPost( platform: $platform postId: $postId hardDelete: $hardDelete ) { success errors { field code message } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "platform": "INSTAGRAM", "postId": "12345", "hardDelete": false } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "deletePlatformPost": { "success": true, "errors": [] } } } ``` :::: ::: :: ::field-group :::field{required name="platform" type="SocialPlatform"} The social media platform for this post. ::: :::field{required name="postId" type="ID"} The unique identifier of the post to delete. ::: :::field{name="hardDelete" type="Boolean"} If `true`, permanently deletes the post. If `false` or omitted, moves the post to trash (soft delete). Default is `false`. ::: :: ::note Soft-deleted posts can be restored from the trash. Hard-deleted posts are permanently removed and cannot be recovered. :: ## Cancel Platform Auto Repost Cancel the automatic reposting schedule for a submission. This stops a submission from being automatically reposted in the future. ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation cancelPlatformAutoRepost( $platform: SocialPlatform! $submissionId: ID! ) { cancelPlatformAutoRepost( platform: $platform submissionId: $submissionId ) { success errors { code message } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "platform": "TWITTER", "submissionId": "67890" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "cancelPlatformAutoRepost": { "success": true, "errors": [] } } } ``` :::: ::: :: ::field-group :::field{required name="platform" type="SocialPlatform"} The social media platform for this submission. ::: :::field{required name="submissionId" type="ID"} The unique identifier of the submission with auto-repost enabled. ::: :: ::note This mutation is only supported for platforms that support auto-reposting. You'll receive an error if the platform doesn't support this feature. :: ## Cancel Platform Auto Remove Cancel the automatic removal schedule for a submission. This prevents a submission from being automatically deleted from the platform at a scheduled time. ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation cancelPlatformAutoRemove( $platform: SocialPlatform! $submissionId: ID! ) { cancelPlatformAutoRemove( platform: $platform submissionId: $submissionId ) { success errors { code message } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "platform": "REDDIT", "submissionId": "67890" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "cancelPlatformAutoRemove": { "success": true, "errors": [] } } } ``` :::: ::: :: ::field-group :::field{required name="platform" type="SocialPlatform"} The social media platform for this submission. ::: :::field{required name="submissionId" type="ID"} The unique identifier of the submission with auto-remove enabled. ::: :: ::note This mutation is only supported for platforms that support auto-removal (like Reddit). You'll receive an error if the platform doesn't support this feature. :: ## Save Platform Submission As Draft Convert a scheduled submission back to draft status. This allows you to make changes before publishing. ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation savePlatformSubmissionAsDraft( $platform: SocialPlatform! $submissionId: ID! ) { savePlatformSubmissionAsDraft( platform: $platform submissionId: $submissionId ) { success errors { field code message } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "platform": "LINKEDIN", "submissionId": "67890" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "savePlatformSubmissionAsDraft": { "success": true, "errors": [] } } } ``` :::: ::: :: ::field-group :::field{required name="platform" type="SocialPlatform"} The social media platform for this submission. ::: :::field{required name="submissionId" type="ID"} The unique identifier of the submission to convert to draft. ::: :: ::note The submission must not have been published yet. Only scheduled submissions can be saved as drafts. :: # Post Tags ## Post Tags Post tags allow you to organize and categorize your scheduled posts. You can query your post tags via the API to use them when scheduling posts across any platform. ::tip Post tags are referenced by their ID when scheduling posts. Use the `postTags` query to retrieve your available tags and their IDs. :: ## Query Use the `postTags` query to retrieve all post tags for your account: ```graphql query postTags($activeOnly: Boolean) { postTags(activeOnly: $activeOnly) { id name color dateArchived dateCreated dateUpdated } } ``` ### Parameters ::field-group :::field{name="activeOnly" type="Boolean"} When `true`, returns only active (non-archived) tags. When `false`, returns all tags including archived ones. Defaults to `true`. ::: :: ### Response Fields ::field-group :::field{name="id" type="ID!"} The unique identifier for the post tag. Use this ID when assigning tags to scheduled posts. ::: :::field{name="name" type="String!"} The display name of the tag. ::: :::field{name="color" type="String!"} The hex color code associated with the tag (e.g., `#FF5733`). ::: :::field{name="dateArchived" type="DateTime"} The date when the tag was archived, or `null` if the tag is active. ::: :::field{name="dateCreated" type="DateTime!"} The date when the tag was created. ::: :::field{name="dateUpdated" type="DateTime!"} The date when the tag was last updated. ::: :: ## Examples ### Get All Active Tags ```graphql query { postTags { id name color } } ``` **Response:** ```json { "data": { "postTags": [ { "id": "123", "name": "Product Launch", "color": "#4CAF50" }, { "id": "456", "name": "Blog Content", "color": "#2196F3" } ] } } ``` ## Using Tags When Scheduling Posts When scheduling posts to any platform, you can include the `postTags` parameter with an array of tag IDs: ```graphql mutation ScheduleTweet($input: ScheduleTweetInput!) { scheduleTweet(input: $input) { success errors { message } } } ``` ```json [Variables] { "input": { "username": "myhandle", "postAt": "2025-12-15T14:30:00.000Z", "thread": [{ "text": "Check out our new product!", "order": 0 }], "postTags": ["123", "456"] } } ``` ::note The `postTags` parameter accepts an array of post tag IDs. You can assign multiple tags to a single post. :: ## Creating a Post Tag Use the `createPostTag` mutation to create a new post tag: ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation CreatePostTag($input: PostTagInputType!) { createPostTag(input: $input) { success errors { field message } postTag { id name color } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "input": { "name": "Product Launch", "color": "#4CAF50" } } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "createPostTag": { "success": true, "errors": [], "postTag": { "id": "789", "name": "Product Launch", "color": "#4CAF50" } } } } ``` :::: ::: :: ### PostTagInputType Fields ::field-group :::field{required name="name" type="String!"} Display name for the tag. ::: :::field{required name="color" type="String!"} Hex color code for the tag (e.g., `#4CAF50`). ::: :: ## Updating a Post Tag Use the `updatePostTag` mutation to update an existing post tag's name or color: ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation UpdatePostTag($input: UpdatePostTagInputType!) { updatePostTag(input: $input) { success errors { field message } postTag { id name color } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "input": { "id": "789", "name": "Product Launch 2.0" } } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "updatePostTag": { "success": true, "errors": [], "postTag": { "id": "789", "name": "Product Launch 2.0", "color": "#4CAF50" } } } } ``` :::: ::: :: ### UpdatePostTagInputType Fields ::field-group :::field{required name="id" type="ID!"} The ID of the tag to update. ::: :::field{name="name" type="String"} New display name for the tag. If not provided, the current name is kept. ::: :::field{name="color" type="String"} New hex color code for the tag. If not provided, the current color is kept. ::: :: ## Archiving a Post Tag Use the `archivePostTag` mutation to archive a post tag. Archived tags are hidden by default but can be restored later. ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation ArchivePostTag($postTagId: ID!) { archivePostTag(postTagId: $postTagId) { success errors { field message } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "postTagId": "789" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "archivePostTag": { "success": true, "errors": [] } } } ``` :::: ::: :: ::field-group :::field{required name="postTagId" type="ID!"} The unique identifier of the post tag to archive. ::: :: ## Restoring a Post Tag Use the `restorePostTag` mutation to restore a previously archived post tag: ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation RestorePostTag($postTagId: ID!) { restorePostTag(postTagId: $postTagId) { success errors { field message } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "postTagId": "789" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "restorePostTag": { "success": true, "errors": [] } } } ``` :::: ::: :: ::field-group :::field{required name="postTagId" type="ID!"} The unique identifier of the archived post tag to restore. ::: :: ## Deleting a Post Tag Use the `deletePostTag` mutation to permanently delete a post tag: ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation DeletePostTag($postTagId: ID!) { deletePostTag(postTagId: $postTagId) { success errors { field message } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "postTagId": "789" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "deletePostTag": { "success": true, "errors": [] } } } ``` :::: ::: :: ::field-group :::field{required name="postTagId" type="ID!"} The unique identifier of the post tag to permanently delete. ::: :: ::warning This action permanently deletes the tag and removes it from all analytics submissions. This cannot be undone. :: # Retrieving Posts ## Overview The Postpone API provides GraphQL queries to retrieve your posts across all supported platforms. Each platform has two types of queries: - **Single post query** — Fetch a specific post by its ID. - **Submissions listing query** — List submissions with filtering, sorting, and pagination. ::tip Use the single post query when you know the post ID (e.g., after scheduling). Use the submissions listing query to browse, search, and filter your posts. :: ## Single Post Queries Fetch a single post by its unique ID. Each platform has its own query: | Platform | Query | | --------- | ------------------------ | | Reddit | `redditPost(id: ID!)` | | X/Twitter | `twitterPost(id: ID!)` | | Instagram | `instagramPost(id: ID!)` | | Facebook | `facebookPost(id: ID!)` | | Threads | `threadsPost(id: ID!)` | | Bluesky | `blueskyPost(id: ID!)` | | TikTok | `tiktokPost(id: ID!)` | | YouTube | `youtubePost(id: ID!)` | | LinkedIn | `linkedinPost(id: ID!)` | | Pinterest | `pinterestPost(id: ID!)` | | Mastodon | `mastodonPost(id: ID!)` | | Tumblr | `tumblrPost(id: ID!)` | | Telegram | `telegramPost(id: ID!)` | ### Example ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Query"} ```graphql query TwitterPost($id: ID!) { twitterPost(id: $id) { id socialAccount { id username } submissions { id text postAt url result { id dateSubmitted likes comments views } error { errorCode message } } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "id": "12345" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "twitterPost": { "id": "12345", "socialAccount": { "id": "678", "username": "myhandle" }, "submissions": [ { "id": "99001", "text": "Check out our latest blog post!", "postAt": "2025-12-15T14:30:00.000Z", "url": "https://twitter.com/myhandle/status/123456789", "result": { "id": "55001", "dateSubmitted": "2025-12-15T14:30:05.000Z", "likes": 42, "comments": 7, "views": 1250 }, "error": null } ] } } } ``` :::: ::: :: ::note All single post queries return `null` if the post is not found or you don't have access to it. :: ## Listing Submissions List submissions with pagination, filtering, and sorting. Each platform has its own submissions query: | Platform | Query | | --------- | ---------------------- | | Reddit | `redditSubmissions` | | X/Twitter | `twitterSubmissions` | | Instagram | `instagramSubmissions` | | Facebook | `facebookSubmissions` | | Threads | `threadsSubmissions` | | Bluesky | `blueskySubmissions` | | TikTok | `tiktokSubmissions` | | YouTube | `youtubeSubmissions` | | LinkedIn | `linkedinSubmissions` | | Pinterest | `pinterestSubmissions` | | Mastodon | `mastodonSubmissions` | | Tumblr | `tumblrSubmissions` | | Telegram | `telegramSubmissions` | ### Example ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Query"} ```graphql query TwitterSubmissions( $startDate: DateTime $endDate: DateTime $socialAccountIds: [ID] $search: String $sortField: SortFieldType $page: Int $limit: Int $submissionType: SubmissionType $postTags: [ID] $publishingStatus: PublishingStatusType ) { twitterSubmissions( startDate: $startDate endDate: $endDate socialAccountIds: $socialAccountIds search: $search sortField: $sortField page: $page limit: $limit submissionType: $submissionType postTags: $postTags publishingStatus: $publishingStatus ) { total objects { id text postAt url result { id dateSubmitted likes comments views } error { errorCode message } } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "startDate": "2025-12-01T00:00:00.000Z", "endDate": "2025-12-31T23:59:59.000Z", "submissionType": "SUBMITTED", "sortField": "DATE", "page": 1, "limit": 10 } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "twitterSubmissions": { "total": 47, "objects": [ { "id": "99001", "text": "Check out our latest blog post!", "postAt": "2025-12-15T14:30:00.000Z", "url": "https://twitter.com/myhandle/status/123456789", "result": { "id": "55001", "dateSubmitted": "2025-12-15T14:30:05.000Z", "likes": 42, "comments": 7, "views": 1250 }, "error": null } ] } } } ``` :::: ::: :: ## Common Filtering Parameters The following parameters are shared across all submission listing queries: ::field-group :::field{name="startDate" type="DateTime"} Filter submissions scheduled from this date onward. When `submissionType` is `SUBMITTED`, filters by the submission date instead. ::: :::field{name="endDate" type="DateTime"} Filter submissions scheduled until this date. When `submissionType` is `SUBMITTED`, filters by the submission date instead. ::: :::field{name="socialAccountId" type="ID"} Filter by a specific social account ID. ::: :::field --- name: socialAccountIds type: "[ID]" --- Filter by multiple social account IDs. ::: :::field{name="search" type="String"} Search text within post content. ::: :::field{name="sortField" type="SortFieldType"} Sort results by a specific field. Valid values: `DATE`, `LIKES`, `COMMENTS`. ::: :::field{name="page" type="Int"} Page number for pagination. Default: `1`. ::: :::field{name="limit" type="Int"} Number of results per page. Default: `25`. ::: :::field{name="submissionType" type="SubmissionType"} Filter by submission status. Valid values: `ALL`, `SCHEDULED`, `SUBMITTED`, `FAILED`. ::: :::field --- name: postTags type: "[ID]" --- Filter by post tag IDs. Use the [`postTags` query](https://developers.postpone.app/scheduling-posts/post-tags) to retrieve available tag IDs. ::: :::field{name="publishingStatus" type="PublishingStatusType"} Filter by publishing status. Valid values: `READY_TO_PUBLISH`, `DRAFT`. Default: `READY_TO_PUBLISH`. ::: :: ::note Some platforms may have additional platform-specific parameters beyond the common ones listed above (e.g., `rootsOnly` for Twitter threads, `subreddits` for Reddit). :: ## Pagination All submission listing queries return a paginated response with two fields: ::field-group :::field{name="total" type="Int"} The total number of submissions matching the query filters across all pages. ::: :::field --- name: objects type: "[Submission]" --- An array of submission objects for the current page. ::: :: To paginate through results, increment the `page` parameter while keeping the same `limit`: ```graphql # Page 1: first 25 results twitterSubmissions(page: 1, limit: 25) { total objects { id } } # Page 2: results 26-50 twitterSubmissions(page: 2, limit: 25) { total objects { id } } ``` ::tip Use the `total` field to calculate the number of pages: `totalPages = ceil(total / limit)`. :: # Next Scheduled Dates The `nextScheduleDates` query returns the **next available slot** from each social account's [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""} — the time Postpone would use if you scheduled a new post to that account right now. This is the same suggestion the app uses to pre-fill the date and time on the Schedule Post page, so it's useful for automations that want to place a post at the account's next open slot. It is **not** a lookup of when your already-scheduled posts go out — it looks *forward* for the next free slot, taking your existing scheduled posts into account. ::tip If all you want is to place a post at the next open slot, you don't need this query: pass [`schedulingMode: QUEUE_NEXT`](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot) to the scheduling mutation instead. It picks and takes the slot in a single request. Use `nextScheduleDates` when you want to *show* the upcoming slot — to pre-fill a date picker, say — rather than post to it. :: ## What You Get Back `scheduleDate` is the earliest upcoming slot in the account's schedule that isn't already taken by a scheduled post. A slot is considered taken when a scheduled post falls on it or within about 30 minutes of it, so the returned time is always clear of your existing posts. `scheduleDate` is `null` only when the account has no usable schedule — no slots, or no timezone set. A schedule with slots always returns a date: the search runs as far ahead as your queue requires, so an account booked solid for months returns the first slot past the end of the queue rather than nothing. Reddit accounts are supported and return a `scheduleDate` like any other account. A Reddit schedule belongs to the account rather than to any one subreddit, so a slot counts as taken when **any** submission from that account falls near it, whichever subreddit it goes to — a post going out to several subreddits doesn't take one slot per subreddit. It does take more than one when its submissions are spaced out far enough: each submission is a scheduled post in its own right, so one that lands within about 30 minutes of a later slot takes that slot too. The date returned for a Reddit account always comes from the account schedule. In the app, a subreddit that has a schedule of its own overrides it, so the slot you get here can differ from the date the app would pre-fill for that subreddit. ## AccountScheduleDateType ::field-group :::field{name="socialAccountId" type="ID"} The ID of the social account. ::: :::field{name="scheduleDate" type="DateTime"} The next available slot from this account's schedule, or `null` if the account has no usable schedule. ::: :: ## nextScheduleDates Query ::field-group :::field --- name: socialAccountIds type: "[ID]" --- List of social account IDs to query. Use the `socialAccounts` query to retrieve IDs for your connected accounts. See [Social Accounts](https://developers.postpone.app/queries/social-accounts). ::: :: ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Query"} ```graphql query nextScheduleDates($socialAccountIds: [ID]) { nextScheduleDates(socialAccountIds: $socialAccountIds) { socialAccountId scheduleDate } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "socialAccountIds": ["789", "790"] } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "nextScheduleDates": [ { "socialAccountId": "789", "scheduleDate": "2025-01-16T09:00:00Z" }, { "socialAccountId": "790", "scheduleDate": null } ] } } ``` :::: ::: :: ::note Every account you asked for and manage is returned, even when it has no schedule set up — in that case `scheduleDate` is `null` (account `790` above). To place a post at the returned slot, pass it as the scheduled time when creating the post — or skip the round trip and let the mutation do it with [`schedulingMode: QUEUE_NEXT`](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot). :: # Uploading Media The API offers two ways to upload media to your Content Library: - **From a URL**: `uploadMediaFromUrl`, when your file is already hosted at a public URL. - **From a local file**: `startMediaUpload` + `completeMediaUpload`, when the file lives on disk. Images, videos, and GIFs are supported. Direct uploads are capped at **500 MiB** per file. ## Uploading Media from a URL Adds a file to your Content Library from a public URL. ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation uploadMediaFromUrl( $url: String! $name: String $mediaLabelIds: [ID] ) { uploadMediaFromUrl( url: $url name: $name mediaLabelIds: $mediaLabelIds ) { success media { id name url mimeType size } errors { field code message } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "url": "https://example.com/images/hero.jpg", "name": "campaign-hero.jpg", "mediaLabelIds": ["1182", "1235"] } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "uploadMediaFromUrl": { "success": true, "media": { "id": "1001", "name": "campaign-hero.jpg", "url": "https://cdn.example.com/1a2b3c4d/", "mimeType": "image/jpeg", "size": 482912 }, "errors": [] } } } ``` :::: ::: :: ::field-group :::field{required name="url" type="String"} Public URL of the media file. Must be publicly accessible and link directly to the file. ::: :::field{name="name" type="String"} Display name for the file in your Content Library. If omitted, the name is derived from the filename in the `url`. ::: :::field --- name: mediaLabelIds type: "[ID]" --- IDs of existing Content Library labels to assign. Labels must already exist and belong to your account. Unknown or foreign IDs are ignored. ::: :: ## Direct Upload When your file is on disk, upload it in three steps. ::steps{level="3"} ### Start the upload Call `startMediaUpload` with the exact file size in bytes and the file's mime type. It returns an `uploadUrl` and an `uploadToken`. :::code-collapse ::::tabs :::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation startMediaUpload($size: BigInt!, $mimeType: String!) { startMediaUpload(size: $size, mimeType: $mimeType) { success uploadToken uploadUrl expiresAt errors { field code message } } } ``` ::::: :::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "size": 2048576, "mimeType": "image/jpeg" } ``` ::::: :::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "startMediaUpload": { "success": true, "uploadToken": "b3f1c2a4-9d5e-4c7b-8a1f-2e6d0c9a7b12", "uploadUrl": "https://uploads.example.com/...&signature=...", "expiresAt": "2025-02-01T15:30:00Z", "errors": [] } } } ``` ::::: :::: ::: :::field-group ::::field{required name="size" type="BigInt"} Size of the file in bytes. Must be greater than 0 and no more than 500 MiB. The upload URL is tied to this exact size, so the `PUT` in the next step must send a matching `Content-Length`. :::: ::::field{required name="mimeType" type="String"} Mime type of the file. Must be an image or a video type, such as `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `image/heic`, `video/mp4`, or `video/quicktime`. The upload URL is tied to this exact value, so the `PUT` in the next step must send it as the `Content-Type` header, verbatim. :::: ::: ### Upload the file Send the raw file bytes to `uploadUrl` with an HTTP `PUT`. Three headers are required: - `Content-Length` **must** match the `size` you passed to `startMediaUpload`. The URL is tied to the declared size, and a mismatch is rejected. - `Content-Type` **must** match the `mimeType` you passed to `startMediaUpload`, verbatim. This is what makes the file's type detectable once it lands in your Content Library. - `If-None-Match: *` **must** be sent exactly as shown. The URL is signed with it, and requests without it are rejected. :::code-group ```bash [cURL] curl --request PUT \ --upload-file ./campaign-hero.jpg \ --header 'Content-Length: 2048576' \ --header 'Content-Type: image/jpeg' \ --header 'If-None-Match: *' \ "https://uploads.example.com/...&signature=..." ``` ```python [Python] import requests with open('./campaign-hero.jpg', 'rb') as f: response = requests.put( 'https://uploads.example.com/...&signature=...', data=f, headers={ 'Content-Type': 'image/jpeg', 'If-None-Match': '*', }, ) response.raise_for_status() ``` ```js [JavaScript/Node.js] import { readFile } from 'node:fs/promises'; const file = await readFile('./campaign-hero.jpg'); const response = await fetch('https://uploads.example.com/...&signature=...', { method: 'PUT', headers: { 'Content-Type': 'image/jpeg', 'If-None-Match': '*', }, body: file, }); if (!response.ok) { throw new Error(`Upload failed: ${response.status}`); } ``` ::: :::note Most HTTP clients (including `curl` with `--upload-file`) set `Content-Length` automatically from the file. Only set it by hand if your client doesn't. ::: :::note The `uploadUrl` is valid until `expiresAt`, roughly an hour after it's issued. Upload the file and call `completeMediaUpload` before then, or start over. ::: :::note Each `uploadUrl` accepts **one successful upload**. If the transfer fails partway through, just retry the same `PUT`. A `412 Precondition Failed` response means a file was already uploaded with this URL. No need to retry, move on to `completeMediaUpload`. ::: ### Complete the upload Once you finished uploading the file, you must call `completeMediaUpload` with the `uploadToken` and a display `name`. This adds the uploaded file to your Content Library and returns the created media. :::code-collapse ::::tabs :::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation completeMediaUpload( $uploadToken: String! $name: String! $mediaLabelIds: [ID] ) { completeMediaUpload( uploadToken: $uploadToken name: $name mediaLabelIds: $mediaLabelIds ) { success media { id name url mimeType size } errors { field code message } } } ``` ::::: :::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "uploadToken": "b3f1c2a4-9d5e-4c7b-8a1f-2e6d0c9a7b12", "name": "campaign-hero.jpg", "mediaLabelIds": ["12"] } ``` ::::: :::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "completeMediaUpload": { "success": true, "media": { "id": "102", "name": "campaign-hero.jpg", "url": "https://cdn.example.com/5e6f7a8b/", "mimeType": "image/jpeg", "size": 2048576 }, "errors": [] } } } ``` ::::: :::: ::: :::field-group ::::field{required name="uploadToken" type="String"} The token returned by `startMediaUpload`. :::: ::::field{required name="name" type="String"} Display name for the file in your Content Library. A directly uploaded file carries no filename of its own, so this is the only name the file gets. It is also what you pass as `name` if you reference the file by name when scheduling. :::: ::::field --- name: mediaLabelIds type: "[ID]" --- IDs of existing Content Library labels to assign. :::: ::: :::note `completeMediaUpload` transfers the uploaded file into your Content Library, so the request takes longer for bigger files. Large videos may not finish in time, in which case the request fails with an upload timeout error or an HTTP `504`. When that happens, wait a few seconds and call `completeMediaUpload` again with the same `uploadToken`. Retry until it returns `success: true` or a different error. Retrying is safe: if an earlier attempt already added the file to your library, the same media is returned instead of a duplicate. ::: :: ::warning An upload token maps to a single file: once completed, calling `completeMediaUpload` with the same token again returns the same media rather than creating a new one. Call `startMediaUpload` again for the next file. If a completion attempt fails, you can retry it while the upload is still valid (until `expiresAt`). :: ## Scheduling a Post with the Uploaded Media Both upload paths return the created `media`, including its `id`. Pass that `id` in the `media` list of any scheduling mutation to attach the file to a post: ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation scheduleTweet($input: ScheduleTweetInput!) { scheduleTweet(input: $input) { success errors { field code message } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Our new campaign is live 🚀", "order": 0, "media": [{ "id": "1001" }] } ] } } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "scheduleTweet": { "success": true, "errors": [] } } } ``` :::: ::: :: You can also reference the file by the `name` you gave it when uploading, which is handy when you don't keep track of IDs: ```json { "media": [{ "name": "campaign-hero.jpg" }] } ``` Names are matched case-insensitively and the first match wins, so prefer `id` when several files share a name. Uploaded files stay in your Content Library, so the same `id` can be attached to as many posts as you like. Uploading is only needed once per file. ::note Uploading is optional. If your file is already at a public URL and you don't need it in your Content Library beforehand, you can always skip these mutations and pass `{ "url": "https://example.com/images/hero.jpg" }` directly in `media`. :: See [Attaching Media](https://developers.postpone.app/scheduling-posts#attaching-media) for the full rules on `media`, including how many files each platform accepts. ::tip Uploading a video? Set its cover image with [`setVideoCover`](https://developers.postpone.app/scheduling-posts/video-covers) before you schedule it. :: ## Rate Limits Media uploads count against a **separate, stricter budget** on top of the [general API rate limit](https://developers.postpone.app/getting-started/rate-limits): ::card-group :::card{icon="i-lucide-user" title="Free Accounts"} **10 uploads** per hour ::: :::card{icon="i-lucide-crown" title="Paid Accounts"} **100 uploads** per hour ::: :: An upload counts against this budget when it begins, that is, on each `uploadMediaFromUrl` or `startMediaUpload` call. When you exceed these limits, the mutation returns `success: false` with a `Media upload rate limit exceeded` error. The two budgets are tracked separately, so exhausting your upload budget never blocks your regular API requests. They aren't mutually exclusive, though: like every API call, an upload mutation also counts against the general rate limit. ## Error Handling The mutations return `success: false` with details in `errors` when there's a failure. Always check `success` and inspect `errors` before using the returned `media`. # Video Covers A video's cover is the still image people see before they press play. Use `setVideoCover` to choose a video's cover in one of two ways: - **A frame of the video**: pass `offsetMs`, the position of the frame in milliseconds. - **Your own image**: pass `imageUrl` to use an image at a public URL, or `imageMediaId` to use an image from your Content Library. The cover belongs to the video in your Content Library, not to a single post. Every post that uses the video gets the cover, including posts that are already scheduled, as long as the cover is set before the post publishes. This is the same cover you can pick in the Postpone web app. ## Supported Platforms | Platform | Post types | Frame of the video (`offsetMs`) | Your own image (`imageUrl`, `imageMediaId`) | | --------- | ---------- | ------------------------------- | ------------------------------------------- | | Instagram | Reels | ✅ | ✅ | | TikTok | Videos | ✅ | ❌ Uses the first frame instead | | Facebook | Reels | ✅ | ✅ | | LinkedIn | Videos | ✅ | ✅ | | Pinterest | Video Pins | ✅ | ✅ | Covers are ignored everywhere else, including Instagram Stories and carousels, Facebook Stories, and platforms not listed above, such as YouTube, X (Twitter), and Threads. On those, the platform picks the cover itself. ::tip Prefer `offsetMs` when you post the same video to several platforms. It's the only option that works on TikTok, and Instagram and TikTok pick the frame from the video themselves at that offset. For Facebook, LinkedIn, and Pinterest, Postpone captures the frame at that offset as an image and uploads it as the cover. :: ## Setting a Cover ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation setVideoCover( $mediaId: ID! $offsetMs: Int $imageUrl: String $imageMediaId: ID ) { setVideoCover( mediaId: $mediaId offsetMs: $offsetMs imageUrl: $imageUrl imageMediaId: $imageMediaId ) { success media { id name coverImage { url offsetMs } } errors { field code message } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "mediaId": "1002", "offsetMs": 2500 } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "setVideoCover": { "success": true, "media": { "id": "1002", "name": "product-launch.mp4", "coverImage": { "url": "https://cdn.example.com/9c8b7a6d/", "offsetMs": 2500 } }, "errors": [] } } } ``` :::: ::: :: ::field-group :::field{required name="mediaId" type="ID"} ID of the video in your Content Library. Only videos can have a cover. ::: :::field{name="offsetMs" type="Int"} Use the frame at this position in the video as the cover, in milliseconds. Must be `0` or more, and at least 100 ms before the end of the video's `duration`, since the last frame starts before the video ends. For example, `2500` uses the frame 2.5 seconds into the video. ::: :::field{name="imageUrl" type="String"} Public URL of a JPEG, PNG, or WebP image to use as the cover. Other formats, such as GIF and SVG, aren't supported. Must be publicly accessible and link directly to the file. ::: :::field{name="imageMediaId" type="ID"} ID of a JPEG, PNG, or WebP image in your Content Library to use as the cover. The image stays in your Content Library, and the video gets its own copy as its cover. ::: :: Pass **exactly one** of `offsetMs`, `imageUrl`, or `imageMediaId`. Setting a cover replaces the video's existing cover, if it has one. The returned `coverImage.url` is always an image you can show as a preview. For `offsetMs` covers, it's the frame captured at that offset. ### Using an Image from a URL ```json { "mediaId": "1002", "imageUrl": "https://example.com/images/launch-cover.jpg" } ``` The image doesn't need to match the video's size, but platforms crop covers that don't match the video's aspect ratio. Use an image with the same aspect ratio as the video for the best results. ### Using a Local Image To use an image file on disk as a cover, first add it to your Content Library with a [direct upload](https://developers.postpone.app/scheduling-posts/uploading-media#direct-upload), then pass the returned media `id` as `imageMediaId`: ```json { "mediaId": "1002", "imageMediaId": "1003" } ``` ## Removing a Cover `removeVideoCover` removes the video's cover. Posts that use the video go back to the cover each platform picks by default. Calling it on a video without a cover is a no-op that succeeds. ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Mutation"} ```graphql mutation removeVideoCover($mediaId: ID!) { removeVideoCover(mediaId: $mediaId) { success media { id coverImage { url } } errors { field code message } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "mediaId": "1002" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "removeVideoCover": { "success": true, "media": { "id": "1002", "coverImage": null }, "errors": [] } } } ``` :::: ::: :: ::field-group :::field{required name="mediaId" type="ID"} ID of the video in your Content Library. ::: :: ## Reading a Video's Cover The `coverImage` field of [MediaType](https://developers.postpone.app/queries/content-library#mediatype) returns the video's current cover, or `null` when it has none: ```graphql query media($ids: [ID]) { media(ids: $ids) { objects { id duration coverImage { url offsetMs } } } } ``` `offsetMs` is set for covers chosen from a frame of the video and `null` for covers made from your own image. ## Scheduling a Reel with a Cover Covers are set on Content Library videos, so the video has to be in your Content Library before you set its cover. ::steps{level="3"} ### Upload the video Upload the video with [`uploadMediaFromUrl`](https://developers.postpone.app/scheduling-posts/uploading-media#uploading-media-from-a-url) or a [direct upload](https://developers.postpone.app/scheduling-posts/uploading-media#direct-upload), and keep the returned media `id`. If the video is already in your Content Library, find its `id` with the [`media` query](https://developers.postpone.app/queries/content-library) instead. ### Set the cover Call `setVideoCover` with the video's `id` as `mediaId`: ```json { "mediaId": "1002", "offsetMs": 2500 } ``` ### Schedule the post Pass the video's `id` in `media` when you schedule the post. For example, an Instagram Reel: ```json { "input": { "username": "myhandle", "caption": "Our new collection is here ✨", "media": [{ "id": "1002" }], "submissions": [ { "postAt": "2026-12-01T15:30:00Z", "mediaType": "REEL" } ] } } ``` See [Instagram](https://developers.postpone.app/scheduling-posts/platforms/instagram) for the full `scheduleInstagramPost` mutation. The post publishes with the cover. You can also change the cover after scheduling, as long as you do it before the post publishes. :: ::note If you attach a video by `url` when scheduling, Postpone adds it to your Content Library at that point. To give it a cover, find it with the [`media` query](https://developers.postpone.app/queries/content-library) and call `setVideoCover` before the post publishes, or upload it first as shown above. :: ## Rate Limits Each `setVideoCover` call that passes validation uploads an image, so it counts against the [media upload rate limit](https://developers.postpone.app/scheduling-posts/uploading-media#rate-limits), along with the general API rate limit. `removeVideoCover` only counts against the general API rate limit. ## Common Errors | Error | Cause | | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `Provide exactly one of: offsetMs, imageUrl, imageMediaId.` | None or more than one cover option was passed. | | `Media not found.` | `mediaId` doesn't match a file in your Content Library. | | `Covers can only be set on videos.` | `mediaId` is an image or a GIF. | | `offsetMs cannot be negative.` | `offsetMs` is below `0`. | | `offsetMs must be … or less for this video.` | `offsetMs` is at or too close to the end of the video. Check the video's `duration`, in milliseconds, and pick an earlier frame. | | `Cover image media not found.` | `imageMediaId` doesn't match a file in your Content Library. | | `The cover image must be a JPEG, PNG, or WebP image.` | `imageUrl` or `imageMediaId` points to a video, a GIF, an SVG, or a file that isn't an image. | | `Invalid cover image URL.` | `imageUrl` isn't a valid public `http` or `https` URL. | | `Failed to upload the cover image.` | The image at `imageUrl` couldn't be downloaded, or the frame couldn't be captured. Check that the URL is public, then try again. | | `Media upload rate limit exceeded` | You've hit the [media upload rate limit](https://developers.postpone.app/scheduling-posts/uploading-media#rate-limits). | # All Platforms Select a platform below to learn how to schedule posts with platform-specific features and requirements: ## Supported Platforms ::card-group :::card --- ui: icon: text-[#ff4500] icon: i-simple-icons-reddit title: Reddit to: https://developers.postpone.app/scheduling-posts/platforms/reddit --- Schedule notification posts to multiple subreddits with custom timing, flair selection, and more. ::: :::card --- ui: icon: text-[#1a1a18] dark:text-white icon: i-simple-icons-x title: X/Twitter to: https://developers.postpone.app/scheduling-posts/platforms/twitter --- Schedule tweets with media attachments and thread support. ::: :::card --- ui: icon: text-[#e1306c] icon: i-simple-icons-instagram title: Instagram to: https://developers.postpone.app/scheduling-posts/platforms/instagram --- Schedule photo and video posts with captions and hashtags. ::: :::card --- ui: icon: text-[#1877f2] icon: i-simple-icons-facebook title: Facebook to: https://developers.postpone.app/scheduling-posts/platforms/facebook --- Schedule posts to Facebook Pages. ::: :::card --- ui: icon: text-[#000000] dark:text-white icon: i-simple-icons-threads title: Threads to: https://developers.postpone.app/scheduling-posts/platforms/threads --- Schedule posts to Meta's Threads platform. ::: :::card --- ui: icon: text-[#0085ff] icon: i-simple-icons-bluesky title: Bluesky to: https://developers.postpone.app/scheduling-posts/platforms/bluesky --- Schedule posts to the decentralized Bluesky network. ::: :::card --- ui: icon: text-[#000000] dark:text-white icon: i-simple-icons-tiktok title: TikTok to: https://developers.postpone.app/scheduling-posts/platforms/tiktok --- Schedule video content to TikTok. ::: :::card --- ui: icon: text-[#ff0000] icon: i-simple-icons-youtube title: YouTube Shorts to: https://developers.postpone.app/scheduling-posts/platforms/youtube --- Schedule short-form video content to YouTube. ::: :::card --- ui: icon: text-[#0077b5] icon: i-simple-icons-linkedin title: LinkedIn to: https://developers.postpone.app/scheduling-posts/platforms/linkedin --- Schedule professional posts to LinkedIn profiles and pages. ::: :::card --- ui: icon: text-[#e60023] icon: i-simple-icons-pinterest title: Pinterest to: https://developers.postpone.app/scheduling-posts/platforms/pinterest --- Schedule pins to Pinterest boards. ::: :::card --- ui: icon: text-[#6364ff] icon: i-simple-icons-mastodon title: Mastodon to: https://developers.postpone.app/scheduling-posts/platforms/mastodon --- Schedule posts to Mastodon instances. ::: :::card --- ui: icon: text-[#001935] dark:text-white icon: i-simple-icons-tumblr title: Tumblr to: https://developers.postpone.app/scheduling-posts/platforms/tumblr --- Schedule posts to Tumblr blogs. ::: :::card --- ui: icon: text-[#26a5e4] icon: i-simple-icons-telegram title: Telegram to: https://developers.postpone.app/scheduling-posts/platforms/telegram --- Schedule posts to Telegram channels and groups. ::: :: # Reddit ::note **Notification Posting Only:** All scheduled Reddit posts use [notification posting](https://help.postpone.app/en/scheduling-posts/reddit-notification-posting){rel=""nofollow""} in accordance with Reddit's rules. When it's time to post, you'll receive a notification to complete the submission natively through Reddit's app or website. :: ## Mutation Use the `scheduleRedditPost` mutation to schedule posts to Reddit: ```graphql [Mutation] mutation ScheduleRedditPost($input: ScheduleRedditPostInput!) { scheduleRedditPost(input: $input) { success errors { field message } post { id socialAccount { id username } publishingStatus title content link nsfw spoiler submissions { id subreddit postAt title comment flairId publishingMethod } } } } ``` ## Input Parameters ### ScheduleRedditPostInput ::field-group :::field{required name="username" type="String!"} The username of the connected Reddit account to post from ::: :::field{required name="title" type="String!"} The title of the Reddit post (max 300 characters) ::: :::field --- required: true name: submissions type: "[RedditPostSubmissionInput!]!" --- Array of subreddit submissions (minimum 1, maximum 50 per post) ::: :::field{name="content" type="String"} Text content for text posts (cannot be combined with link) ::: :::field{name="link" type="String"} URL for link posts (cannot be combined with content) ::: :::field{name="comment" type="String"} Optional comment to post after submission (max 10,000 characters) ::: :::field --- name: pollChoices type: "[String!]" --- Array of poll options (2-6 choices, each max 180 characters) ::: :::field{name="pollDuration" type="Int"} Poll duration in days (1-7 days, default: 3) ::: :::field --- name: media type: "[SimpleMediaInput]" --- Media files to attach to the post. Each item references a file by `id`, `name`, or `url`. A single item becomes an image or video post, several items become a gallery. See [Attaching Media](https://developers.postpone.app/scheduling-posts#attaching-media). Only supported when scheduling new posts, not when updating existing ones. ::: :::field{name="sendRepliesToInbox" type="Boolean"} Whether to receive reply notifications (default: true) ::: :::field{name="nsfw" type="Boolean"} Mark post as NSFW/18+ content (default: false) ::: :::field{name="spoiler" type="Boolean"} Mark post as containing spoilers (default: false) ::: :::field{name="publishingStatus" type="PublishingStatusType"} Publishing status. Options: `READY_TO_PUBLISH`, `DRAFT` (default: `READY_TO_PUBLISH`) ::: :::field{name="schedulingMode" type="SchedulingModeType"} How the post dates are decided. Options: `SPECIFIC_TIME`, `QUEUE_NEXT` (default: `SPECIFIC_TIME`). Only accepted when scheduling a new post. See [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ::: :::field --- name: postTags type: "[ID!]" --- Array of post tag IDs to organize your content. See [Post Tags](https://developers.postpone.app/scheduling-posts/post-tags). ::: :::field{name="skipPostRequirementsValidation" type="Boolean"} Skip subreddit rule validation (default: false) ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ### RedditPostSubmissionInput Each submission to a subreddit supports these parameters: ::field-group :::field{required name="validationId" type="String!"} Internal ID to map response validation errors to specific submissions ::: :::field{required name="subreddit" type="String!"} Name of the subreddit to post to (without r/ prefix) ::: :::field{name="postAt" type="DateTime"} When to publish this submission (must be in the future). Required unless you pass `schedulingMode: QUEUE_NEXT` on the post when scheduling a new one. ::: :::field{name="title" type="String"} Title override for this specific subreddit (max 300 characters) ::: :::field{name="comment" type="String"} Comment override for this specific subreddit ::: :::field{name="overrideComment" type="Boolean"} Whether to override the post-level comment (default: false) ::: :::field{name="flairId" type="String"} Subreddit flair ID to apply to the post ::: :::field{name="publishingMethod" type="PublishingMethodType"} How to publish. Unlike other platforms, Reddit supports notification posting only Options: `MANUAL` (default: `MANUAL`) ::: :::field{name="id" type="ID"} ID of existing submission when updating (only for updates) ::: :: ## Scheduling at the Next Open Slot Instead of picking times yourself, pass `schedulingMode: QUEUE_NEXT` and leave `postAt` off every submission. Postpone puts the post in the next open slot on the account's [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""}, skipping any slot that already has a post on it. Reddit works a little differently from the other platforms here, because its submissions are scheduled independently rather than sharing one date: - The **first submission you list** takes the slot. - Every submission after it is spaced **20 minutes** behind the one before, so the post doesn't land on every subreddit at once. So the order of your `submissions` array decides which subreddit leads. ```json { "input": { "username": "myredditaccount", "title": "Check out my new project", "content": "Here's what I've been working on...", "schedulingMode": "QUEUE_NEXT", "submissions": [ { "validationId": "1", "subreddit": "postpone" }, { "validationId": "2", "subreddit": "test" } ] } } ``` If the next open slot is Friday at 9:00 AM, the r/postpone submission goes out then and the r/test one at 9:20 AM. ::note `QUEUE_NEXT` reads the **Account Schedule** only. The per-subreddit schedules you set up in the Subreddit Manager are used by the post form in the Postpone app, not by the API. :: The account needs a schedule set up for this to work, `postAt` and `QUEUE_NEXT` can't be combined on any submission, drafts can't be queued, and `updateScheduledRedditPost` rejects `schedulingMode`. See [Scheduling at the Next Open Slot](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot) for the full rules. ## Examples ### Simple Text Post Schedule a basic text post to a single subreddit. ```json { "input": { "username": "myreddituser", "title": "Just discovered this amazing productivity hack!", "content": "I've been using the Pomodoro Technique for a week now and my productivity has increased by 40%. Here's how it works...", "submissions": [{"validationId": "sub1", "subreddit": "productivity", "postAt": "2025-12-01T15:30:00Z"}] } } ``` ### Link Post with Multiple Subreddits Schedule a link post to multiple subreddits with different timing. ::warning **Important:** Always space posts to different subreddits at least 10 minutes apart to help avoid Reddit rate limits and potential shadowbans. :: ```json { "input": { "username": "myreddituser", "title": "New open-source project for developers", "link": "https://github.com/username/awesome-project", "submissions": [ { "validationId": "sub1", "subreddit": "programming", "postAt": "2025-12-01T09:00:00Z" }, { "validationId": "sub2", "subreddit": "opensource", "postAt": "2025-12-01T14:00:00Z" }, { "validationId": "sub3", "subreddit": "webdev", "postAt": "2025-12-02T10:00:00Z" } ] } } ``` ### Image Post with Content Library Media Schedule an image post using media from your Content Library. Reference the file by its `id`, which every upload mutation returns. ```json { "input": { "username": "myreddituser", "title": "My latest photography work", "media": [{ "id": "1041" }], "nsfw": false, "submissions": [ { "validationId": "sub1", "subreddit": "photography", "postAt": "2025-12-01T18:00:00Z", "flairId": "original_content" } ] } } ``` You can also reference the file by name. Names are matched case-insensitively and the first match wins, so prefer `id` when several files share a name. ```json { "media": [{ "name": "sunset-landscape.jpg" }] } ``` ### Post with External Media URL Schedule a post by uploading media from an external URL. ```json { "input": { "username": "myreddituser", "title": "Check out this incredible time-lapse!", "media": [{ "url": "https://example.com/timelapse-video.mp4" }], "submissions": [ { "validationId": "sub1", "subreddit": "timelapse", "postAt": "2025-12-01T20:00:00Z" } ] } } ``` ### Poll Post Schedule a post with a poll for community engagement. ```json { "input": { "username": "myreddituser", "title": "What's your favorite programming language in 2024?", "pollChoices": ["JavaScript", "Python", "Rust", "Go", "TypeScript", "Other"], "pollDuration": 168, "submissions": [ { "validationId": "sub1", "subreddit": "programming", "postAt": "2025-12-01T12:00:00Z" } ] } } ``` ### Post with Link Flair Schedule a post with subreddit-specific link flair: ```json { "input": { "username": "myreddituser", "title": "Tutorial: Building REST APIs with Node.js", "link": "https://myblog.com/nodejs-rest-api-tutorial", "submissions": [ { "validationId": "sub1", "subreddit": "node", "postAt": "2025-12-01T14:00:00Z", "flairName": "Tutorial" } ] } } ``` You can also query available link flair for the subreddit: ```graphql [Query Available Flair] query linkFlair($subreddit: String!) { linkFlair(subreddit: $subreddit) { id text } } ``` ### Post with Custom Comments per Subreddit Schedule a post with different comments for different subreddits. ```json { "input": { "username": "myreddituser", "title": "New web framework comparison", "link": "https://myblog.com/framework-comparison-2024", "comment": "I spent 3 months comparing these frameworks. AMA!", "submissions": [ { "validationId": "sub1", "subreddit": "webdev", "postAt": "2025-12-01T10:00:00Z" }, { "validationId": "sub2", "subreddit": "javascript", "postAt": "2025-12-01T15:00:00Z", "comment": "Focused on JavaScript frameworks in this comparison. Thoughts?", "overrideComment": true } ] } } ``` ## Response Types ### Success Response ```json { "data": { "scheduleRedditPost": { "success": true, "errors": [], "post": { "id": "123", "socialAccount": { "id": "456", "username": "myreddituser" }, "publishingStatus": "READY_TO_PUBLISH", "title": "Just discovered this amazing productivity hack!", "content": "I've been using the Pomodoro Technique...", "link": "", "nsfw": false, "spoiler": false, "submissions": [ { "id": "789", "subreddit": "productivity", "postAt": "2025-12-01T15:30:00Z", "title": "", "comment": "", "flairId": "", "publishingMethod": "MANUAL" } ] } } } } ``` ### Error Response ```json { "data": { "scheduleRedditPost": { "success": false, "errors": [ { "field": "title", "message": "Post titles can't be longer than 300 characters." }, { "field": "submissions", "message": "You must submit to at least one subreddit" } ], "post": null } } } ``` ## Updating a Post To update an existing scheduled post, use the same input type with the corresponding update mutation and include the post `id`: ```graphql [Mutation] mutation UpdateScheduledRedditPost($input: ScheduleRedditPostInput!) { updateScheduledRedditPost(input: $input) { success errors { field message } post { id } } } ``` Pass the `id` of the post you want to update in the input, along with the fields you want to change: ```json { "input": { "id": "12345", "username": "myreddituser", "title": "Updated post title", "submissions": [{"validationId": "sub1", "subreddit": "productivity", "postAt": "2025-12-01T15:30:00Z"}] } } ``` ::note The update mutation uses the same input type as the create mutation. Include the `id` field to specify which post to update. Only scheduled posts that have not been published yet can be updated. :: `postAt` is required on every submission of an update — pass the date a submission already has to leave it where it is. `schedulingMode` is not accepted on an update at all; see [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ## Validation Rules ::note The API enforces Reddit's content policies and technical limitations: :: - **Title Length**: Maximum 300 characters (1,000 with spintax) - **Content**: No length limit for text posts - **Comments**: Maximum 10,000 characters per comment - **Media**: Images up to 20MB, videos up to 1GB - **Galleries**: Maximum 20 media items per gallery - **Polls**: 2-6 options, each max 180 characters, duration specified in hours - **Submissions**: Maximum 100 subreddit submissions per post - **Scheduling**: Posts must be scheduled for future dates only, or queued with `schedulingMode: QUEUE_NEXT` - **Account Limits**: Respects your plan's monthly submission limits ## Common Errors ::accordion :::accordion-item --- icon: i-lucide-alert-circle label: Reddit account is not connected to your Postpone account --- The specified username is not connected to your Postpone account. Connect the account in your settings first. ::: :::accordion-item --- icon: i-lucide-type label: Post titles can't be longer than 300 characters --- Reddit post titles have a strict 300 character limit. Shorten your title to fit within this limit. ::: :::accordion-item --- icon: i-lucide-calendar-x label: Post date cannot be in the past --- The `postAt` timestamp must be in the future. Check your timezone settings and ensure the date is correct. ::: :::accordion-item --- icon: i-lucide-calendar-off label: Provide a post date, or use QUEUE_NEXT --- Every submission of a new post has to say when it goes out: give each one a `postAt`, or pass `schedulingMode: QUEUE_NEXT` on the post. Reddit drafts need a date too — they can't be parked without one. ::: :::accordion-item{icon="i-lucide-calendar-off" label="Provide a post date"} `updateScheduledRedditPost` doesn't accept `schedulingMode`, so every submission you send it has to carry a `postAt`. ::: :::accordion-item --- icon: i-lucide-git-fork label: Pass either a post date or QUEUE_NEXT, not both --- `postAt` and `schedulingMode: QUEUE_NEXT` are mutually exclusive. Drop the `postAt` from every submission to queue the post, or drop the `schedulingMode` to keep your own times. ::: :::accordion-item --- icon: i-lucide-calendar-plus label: has no schedule set up --- `schedulingMode: QUEUE_NEXT` needs an [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""} on the Reddit account you're posting from. Set one up, or pass an explicit `postAt`. ::: :::accordion-item --- icon: i-lucide-pencil-off label: schedulingMode is not supported when updating a post --- `schedulingMode` is only accepted when scheduling a new post. To move a post that is already scheduled, pass a `postAt` to `updateScheduledRedditPost`. ::: :::accordion-item --- icon: i-lucide-file-clock label: Drafts cannot use QUEUE_NEXT --- A draft doesn't hold a slot, so queueing one would hand the same slot to every other queued post. Give the draft a `postAt` instead. ::: :::accordion-item --- icon: i-lucide-list label: You must submit to at least one subreddit --- Every Reddit post must be submitted to at least one subreddit. Add a submission to the `submissions` array. ::: :::accordion-item --- icon: i-lucide-unlink label: Cannot submit both link and content --- Reddit posts can be either text posts (with content) or link posts, but not both. Choose one type per post. ::: :::accordion-item --- icon: i-lucide-image-off label: Cannot submit both content and media/gallery --- Text posts with content cannot include media attachments. Use either text content or media, not both. ::: :::accordion-item --- icon: i-lucide-trending-up label: Post outside of plan limits for the scheduled month --- You've reached your plan's Reddit submission limit for the scheduled month. Upgrade your plan or schedule for a different month. ::: :::accordion-item --- icon: i-lucide-lock label: Subreddit does not exist or is private --- The specified subreddit doesn't exist, is private, or you don't have permission to post there. Verify the subreddit name and your access. ::: :::accordion-item --- icon: i-lucide-shield-alert label: Post does not meet subreddit requirements --- Your post doesn't meet the specific subreddit's posting requirements (karma, account age, flair, etc.). Check the subreddit rules or set `skipPostRequirementsValidation` to true. ::: :: # X/Twitter ## Mutation Use the `scheduleTweet` mutation to schedule tweets to X/Twitter: ```graphql [Mutation] mutation ScheduleTweet($input: ScheduleTweetInput!) { scheduleTweet(input: $input) { success errors { field message } post { id socialAccount { id username } publishingStatus submissions { id text order postAt gallery { id galleryMediaSet { id media { id url mimeType } } } } } } } ``` ## Input Parameters ### ScheduleTweetInput ::field-group :::field{required name="username" type="String!"} The username of the connected X/Twitter account to post from ::: :::field{name="postAt" type="DateTime"} When to publish the tweet (must be in the future). Required unless you pass `schedulingMode: QUEUE_NEXT` when scheduling a new post, or the post is a draft. See [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). ::: :::field{name="schedulingMode" type="SchedulingModeType"} How the post date is decided. Options: `SPECIFIC_TIME`, `QUEUE_NEXT` (default: `SPECIFIC_TIME`). Only accepted when scheduling a new post. See [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ::: :::field --- required: true name: thread type: "[TweetInputType!]!" --- Array of tweets that make up the thread (minimum 1 tweet) ::: :::field{name="forSuperFollowersOnly" type="Boolean"} Whether the tweet should only be visible to Super Followers (default: false) ::: :::field{name="replySettings" type="TwitterReplySettingsType"} Who can reply to the tweet. Options: `EVERYONE`, `FOLLOWING`, `MENTIONED` ::: :::field{name="publishingStatus" type="PublishingStatusType"} Publishing status. Options: `READY_TO_PUBLISH`, `DRAFT` (default: `READY_TO_PUBLISH`) ::: :::field --- name: postTags type: "[ID!]" --- Array of post tag IDs to organize your content. See [Post Tags](https://developers.postpone.app/scheduling-posts/post-tags). ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ### TweetInputType Each tweet in the thread supports these parameters: ::field-group :::field{required name="text" type="String!"} The tweet text content (max 280 characters, 25,000 for verified accounts) ::: :::field{required name="order" type="Int!"} Position of this tweet in the thread (0-based) ::: :::field{name="poll" type="PollInputType"} Poll configuration (cannot be combined with media) ::: :::field --- name: media type: "[SimpleMediaInput]" --- Media files to attach to the tweet. Each item references a file by `id`, `name`, or `url`. See [Attaching Media](https://developers.postpone.app/scheduling-posts#attaching-media). Only supported when scheduling new posts, not when updating existing ones. ::: :::field{name="repostAtAmount" type="Int"} Number of time units before auto-retweeting ::: :::field{name="repostAtUnit" type="String"} Time unit for auto-retweet. Options: `hour`, `day` ::: :::field{name="repostRepeatDays" type="Int"} How many days to repeat the auto-retweet (max 365) ::: :::field{name="repostFromSocialAccount" type="SocialAccountInputType"} Account to retweet from (must be connected to your Postpone account) ::: :::field{name="removeAtAmount" type="Int"} Number of time units before auto-deleting the tweet ::: :::field{name="removeAtUnit" type="String"} Time unit for auto-deletion. Options: `hour`, `day` ::: :::field{name="removeMinLikes" type="Int"} Minimum likes required to prevent auto-deletion ::: :::field{name="final" type="Boolean"} Whether this is the final tweet in the thread (default: false) ::: :::field{name="contentWarningNudity" type="Boolean"} Mark content as containing nudity (default: false) ::: :::field{name="contentWarningViolence" type="Boolean"} Mark content as containing violence (default: false) ::: :::field{name="contentWarningSensitive" type="Boolean"} Mark content as sensitive (default: false) ::: :::field{name="paidPartnership" type="Boolean"} Disclose that the tweet promotes a brand or business (default: false) ::: :::field{name="madeWithAi" type="Boolean"} Disclose that the tweet contains AI-generated content (default: false) ::: :::field{name="id" type="ID"} ID of existing tweet when updating (only for updates) ::: :: ## Examples ### Simple Tweet Schedule a basic tweet with text only. ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Hello world! This is my first scheduled tweet 🚀", "order": 0 } ] } } ``` ### Scheduling at the Next Open Slot Instead of picking a time yourself, pass `schedulingMode: QUEUE_NEXT` and leave `postAt` out. Postpone puts the post in the next open slot on the account's [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""}, skipping any slot that already has a post on it. ```json { "input": { "username": "myhandle", "schedulingMode": "QUEUE_NEXT", "thread": [ { "text": "This one goes out at my next open slot.", "order": 0 } ] } } ``` The whole thread lands on that one slot, the same way an explicit `postAt` applies to every tweet in the thread. The account needs a schedule set up for this to work, `postAt` and `QUEUE_NEXT` can't be combined, and `updateScheduledTweet` rejects `schedulingMode` outright. See [Scheduling at the Next Open Slot](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot) for the full rules. ### Tweet with Media URL Schedule a tweet by uploading media from an external URL. Use this when you have a direct link to an image, video, or GIF that you want to attach. The media will be automatically downloaded and added to your Content Library. ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Check out this amazing sunset! 🌅", "order": 0, "media": [{ "url": "https://example.com/sunset.jpg" }] } ] } } ``` ### Tweet with Content Library Media Schedule a tweet using media already in your Content Library. Use this when you want to reuse media you've previously uploaded. Reference the file by its `id`, which every upload mutation returns. ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Sharing our company logo! 🏢", "order": 0, "media": [{ "id": "1042" }] } ] } } ``` You can also reference the file by name. Names are matched case-insensitively and the first match wins, so prefer `id` when several files share a name. ```json { "media": [{ "name": "company-logo.png" }] } ``` ### Thread with Multiple Tweets Schedule a multi-tweet thread. ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "replySettings": "FOLLOWING", "thread": [ { "text": "🧵 Thread about building great APIs (1/3)", "order": 0 }, { "text": "First, always design your API with the developer experience in mind. Clear documentation and consistent patterns make all the difference.", "order": 1 }, { "text": "Second, implement proper error handling and rate limiting. Your API consumers will thank you when things go wrong (and they will).", "order": 2, "final": true } ] } } ``` ### Tweet with Poll Schedule a tweet with an interactive poll. ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "What's your favorite programming language? 💻", "order": 0, "poll": { "choices": ["JavaScript", "Python", "Go", "Rust"], "durationMinutes": 1440 } } ] } } ``` ### Auto-Retweet Configuration Schedule a tweet with automatic retweeting. ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Don't miss our latest blog post about API best practices! 📖", "order": 0, "repostAtAmount": 2, "repostAtUnit": "hour", "repostRepeatDays": 3, "repostFromSocialAccount": { "username": "mycompany" } } ] } } ``` ### Community Post Schedule a tweet to a specific X/Twitter community. ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "communityId": "1234567890", "thread": [ { "text": "Excited to share this with the developer community! 🎉", "order": 0 } ] } } ``` ## Response Types ### Success Response ```json { "data": { "scheduleTweet": { "success": true, "errors": [], "post": { "id": "123", "socialAccount": { "id": "456", "username": "myhandle" }, "publishingStatus": "READY_TO_PUBLISH", "submissions": [ { "id": "789", "text": "Hello world! This is my first scheduled tweet 🚀", "order": 0, "postAt": "2025-12-01T15:30:00Z", "gallery": null, "poll": null } ] } } } } ``` ### Error Response ```json { "data": { "scheduleTweet": { "success": false, "errors": [ { "field": "text", "message": "You cannot schedule a Tweet longer than 280 characters." } ], "post": null } } } ``` ## Updating a Post To update an existing scheduled post, use the same input type with the corresponding update mutation and include the post `id`: ```graphql [Mutation] mutation UpdateScheduledTweet($input: ScheduleTweetInput!) { updateScheduledTweet(input: $input) { success errors { field message } post { id } } } ``` Pass the `id` of the post you want to update in the input, along with the fields you want to change: ```json { "input": { "id": "12345", "username": "myhandle", "postAt": "2026-01-15T10:30:00Z", "thread": [{"text": "Updated tweet text!", "order": 0}] } } ``` ::note The update mutation uses the same input type as the create mutation. Include the `id` field to specify which post to update. Only scheduled posts that have not been published yet can be updated. :: `postAt` is required on an update, so every update states the date the post goes out on — pass the date it already has to leave it where it is. A draft staying a draft is exempt, and a draft you're promoting to `READY_TO_PUBLISH` needs a date of your choosing; see [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). `schedulingMode` is not accepted on an update at all; see [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ## Validation Rules ::note The API enforces X/Twitter's content policies and technical limitations: :: - **Text Length**: 280 characters for regular accounts, 25,000 for verified accounts - **Media**: Maximum 4 images/videos per tweet, GIFs limited to 15MB - **Video**: Maximum 512MB file size, up to 2 minutes 20 seconds for regular accounts, up to 10 minutes for accounts with long video feature - **Polls**: 2-4 options, each max 25 characters, duration 5 minutes to 7 days. Cannot be combined with media attachments. - **Threads**: No hard limit on thread length - **Scheduling**: Posts must be scheduled for future dates only - **Account Limits**: Respects your plan's monthly tweet limits ## Common Errors ::accordion :::accordion-item --- icon: i-lucide-alert-circle label: Twitter account is not connected to your Postpone account --- The specified username is not connected to your Postpone account. Connect the account in your settings first. ::: :::accordion-item --- icon: i-lucide-type label: Tweet longer than character limit --- The tweet text exceeds the maximum length. Regular accounts have a 280 character limit, verified accounts can post up to 25,000 characters. ::: :::accordion-item --- icon: i-lucide-calendar-x label: Post date cannot be in the past --- The `postAt` timestamp must be in the future. Check your timezone settings. ::: :::accordion-item --- icon: i-lucide-calendar-off label: Provide a post date, or use QUEUE_NEXT --- A new tweet has to say when it goes out: pass a `postAt`, or `schedulingMode: QUEUE_NEXT`. Drafts are exempt: leave `postAt` off one and it is parked on a placeholder date about 30 days out. ::: :::accordion-item{icon="i-lucide-calendar-off" label="Provide a post date"} `schedulingMode` isn't accepted on an update, so a `postAt` is the only way to say when the tweet goes out. You'll also see it when promoting a parked draft to `publishingStatus: READY_TO_PUBLISH` without giving it a date. ::: :::accordion-item --- icon: i-lucide-calendar-clock label: Twitter account has no schedule set up --- `schedulingMode: QUEUE_NEXT` needs an [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""} on the account you're posting from. Set one up, or pass an explicit `postAt`. ::: :::accordion-item --- icon: i-lucide-pencil-off label: schedulingMode is not supported when updating a post --- `schedulingMode` is only accepted by `scheduleTweet`. To move a tweet that is already scheduled, pass a `postAt` to `updateScheduledTweet`. ::: :::accordion-item --- icon: i-lucide-image-off label: Cannot add poll to tweet with media --- X/Twitter doesn't allow polls on tweets with images, videos, or GIFs. Choose either media or a poll. ::: :::accordion-item --- icon: i-lucide-trending-up label: Tweet is over the plan limits for the scheduled month --- You've reached your plan's tweet limit for the scheduled month. Upgrade your plan or schedule for a different month. ::: :: # Instagram ## Mutation Use the `scheduleInstagramPost` mutation to schedule content to Instagram: ```graphql [Mutation] mutation ScheduleInstagramPost($input: ScheduleInstagramPostInput!) { scheduleInstagramPost(input: $input) { success errors { field message } post { id socialAccount { id username } publishingStatus caption firstComment collaborators gallery { id galleryMediaSet { id media { url mimeType } } } submissions { id mediaType shareToFeed postAt publishingMethod } } } } ``` ## Input Parameters ### ScheduleInstagramPostInput ::field-group :::field{required name="username" type="String!"} The username of the connected Instagram account to post from ::: :::field{name="schedulingMode" type="SchedulingModeType"} How the post date is decided. Options: `SPECIFIC_TIME`, `QUEUE_NEXT` (default: `SPECIFIC_TIME`). Only accepted when scheduling a new post. See [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ::: :::field{name="caption" type="String"} The main caption for the post (max 2,200 characters, 3-5 hashtags recommended) ::: :::field{name="firstComment" type="String"} The first comment to be posted automatically with the post ::: :::field --- name: collaborators type: "[String!]" --- Array of Instagram usernames to tag as collaborators (max 3 collaborators) ::: :::field{name="audio" type="InstagramAudioInputType"} An audio track to play over a Reel. Only applied to `REEL` submissions, and only on accounts connected via Facebook. Pass `null` to remove a track from a post. See [Attaching Audio to Reels](https://developers.postpone.app/#attaching-audio-to-reels). ::: :::field --- name: media type: "[SimpleMediaInput]" --- Media files to attach to the post. Each item references a file by `id`, `name`, or `url`. See [Attaching Media](https://developers.postpone.app/scheduling-posts#attaching-media). Only supported when scheduling new posts, not when updating existing ones. ::: :::field --- required: true name: submissions type: "[InstagramPostSubmissionInputType!]!" --- Array of submissions defining when and how to post (minimum 1 submission) ::: :::field{name="publishingStatus" type="PublishingStatusType"} Publishing status. Options: `READY_TO_PUBLISH`, `DRAFT` (default: `READY_TO_PUBLISH`) ::: :::field --- name: postTags type: "[ID!]" --- Array of post tag IDs to organize your content. See [Post Tags](https://developers.postpone.app/scheduling-posts/post-tags). ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ### InstagramPostSubmissionInputType Each submission defines a specific posting schedule and format: ::field-group :::field{name="postAt" type="DateTime"} When to publish the content (must be in the future). Required unless you pass `schedulingMode: QUEUE_NEXT` on the post when scheduling a new one, or the post is a draft. See [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). ::: :::field{required name="mediaType" type="InstagramMediaType!"} Type of Instagram content. Options: `FEED_POST`, `CAROUSEL`, `REEL`, `STORY` ::: :::field{name="shareToFeed" type="Boolean"} Whether to share a Reel to the main feed as well (only applies to Reels, default: false) ::: :::field{name="publishingMethod" type="PublishingMethodType"} How to publish the content. Options: `AUTOMATIC`, `MANUAL` (default: `AUTOMATIC`) ::: :::field{name="id" type="ID"} ID of existing submission when updating (only for updates) ::: :: ### InstagramAudioInputType ::field-group :::field{required name="audioId" type="ID!"} The `audioId` of a track returned by [`instagramAudioSearch`](https://developers.postpone.app/#searching-for-audio) ::: :::field{name="audioVolume" type="Int"} How loud the attached track plays, from 0 to 100 (default: 100) ::: :::field{name="videoVolume" type="Int"} How loud the video's own audio plays, from 0 to 100 (default: 100). Instagram rejects a Reel with attached audio when the video has no audio track of its own, so Postpone sends `0` for those videos regardless of what you pass. ::: :: ## Media Types Instagram supports four different content types, each with specific requirements: ### Feed Post - Single image or video posted to the main Instagram feed - Supports square (1:1), portrait (4:5), or landscape (1.91:1) aspect ratios - Maximum file size: 30MB for videos, 8MB for images ### Carousel - Multiple images and/or videos in a single post (2-10 items) - All media items appear in a swipeable gallery - Mixed media types supported (images and videos together) ### Reel - Short-form vertical video content (9:16 aspect ratio recommended) - Minimum duration: 3 seconds - Can optionally be shared to the main feed using `shareToFeed: true` - Can carry an audio track from Instagram's catalog, see [Attaching Audio to Reels](https://developers.postpone.app/#attaching-audio-to-reels) - Can use a custom cover image or a frame of the video as the cover, see [Video Covers](https://developers.postpone.app/scheduling-posts/video-covers) ### Story - Temporary content that disappears after 24 hours - Single image or video only - Vertical format (9:16) recommended ## Attaching Audio to Reels Reels can be published with a track from Instagram's audio catalog. Search the catalog with `instagramAudioSearch`, then pass the track's `audioId` to the scheduling mutation. Audio is only available on Instagram accounts connected via **Facebook**. Instagram's Audio API is not exposed to accounts connected with Instagram Login, and audio is only applied to `REEL` submissions. ### Searching for Audio The `instagramAudioSearch` query returns the tracks Instagram makes available to third-party tools. This catalog is narrower than what the Instagram app offers, so a song you can find in the app may not be available here. ::field-group :::field{required name="socialAccountId" type="ID!"} The ID of the connected Instagram account to search as. Results depend on the account. ::: :::field{name="searchQuery" type="String"} Keywords to filter by. Trending audio is returned when omitted. ::: :::field{name="audioType" type="InstagramAudioTypeEnum"} Which catalog to search. Options: `MUSIC`, `ORIGINAL_SOUND` (default: `MUSIC`) ::: :: ```graphql [Query] query InstagramAudioSearch($socialAccountId: ID!, $searchQuery: String) { instagramAudioSearch( socialAccountId: $socialAccountId searchQuery: $searchQuery audioType: MUSIC ) { audioId title artist audioType durationInMs coverArtworkUrl downloadUrl previewLink igUsername } } ``` ```json { "data": { "instagramAudioSearch": [ { "audioId": "1234567890123456", "title": "Sunset Drive", "artist": "The Example Band", "audioType": "music", "durationInMs": 187000, "coverArtworkUrl": null, "downloadUrl": null, "previewLink": "https://www.instagram.com/reels/audio/1234567890123456/", "igUsername": null } ] } } ``` ::note An empty list is returned when the account can't use audio — it isn't connected via Facebook, isn't yours, or Instagram's Audio API returned an error. It is not an error response. :: #### InstagramAudioType ::field-group :::field{name="audioId" type="ID"} The identifier to pass as `audio.audioId` when scheduling a Reel. ::: :::field{name="title" type="String"} The track's title. ::: :::field{name="artist" type="String"} The artist for music, or the creator's handle (`@username`) for an original sound. ::: :::field{name="audioType" type="String"} Either `music` or `original_sound`. ::: :::field{name="durationInMs" type="Int"} The length of the whole track in milliseconds. Attached audio always plays from the start. ::: :::field{name="coverArtworkUrl" type="String"} Cover art thumbnail. Absent on licensed music, which carries no artwork. ::: :::field{name="downloadUrl" type="String"} A playable audio file, for previewing the track. Absent on licensed music, and short-lived — re-fetch it with `instagramAudio` rather than storing it. ::: :::field{name="previewLink" type="String"} A link to the track's page on Instagram, for tracks with no playable file. ::: :::field{name="igUsername" type="String"} The Instagram handle behind an original sound. ::: :: ### Looking up a Single Track `instagramAudio` returns one track by ID, which is useful for showing the title of a track already attached to a post, or for refreshing an expired `downloadUrl`. It returns `null` when the ID isn't valid for the account. ```graphql [Query] query InstagramAudio($socialAccountId: ID!, $audioId: String!) { instagramAudio(socialAccountId: $socialAccountId, audioId: $audioId) { audioId title artist durationInMs downloadUrl } } ``` ### Scheduling a Reel with Audio Pass the `audioId` from the search results in the `audio` input, optionally mixing the levels of the track and the video's own sound: ```json { "input": { "username": "myhandle", "caption": "New drop 🔥", "media": [{ "url": "https://example.com/reel.mp4" }], "audio": { "audioId": "1234567890123456", "audioVolume": 80, "videoVolume": 20 }, "submissions": [ { "postAt": "2025-12-01T16:00:00Z", "mediaType": "REEL", "shareToFeed": true } ] } } ``` The post stores only the `audioId`, so `audio` on a scheduled post is read back through the `audioId`, `audioVolume`, and `videoVolume` fields: ```graphql [Query] query { instagramPost(id: "123") { id audioId audioVolume videoVolume } } ``` To remove a track from a scheduled post, pass `"audio": null` to `updateScheduledInstagramPost`. ## Examples ### Feed Post with Single Image Schedule a basic Instagram feed post with an image from URL. ```json { "input": { "username": "myhandle", "caption": "Beautiful sunset from our latest adventure! 🌅 #photography #sunset #travel", "media": [{ "url": "https://example.com/sunset.jpg" }], "submissions": [ { "postAt": "2025-12-01T18:00:00Z", "mediaType": "FEED_POST" } ] } } ``` ### Scheduling at the Next Open Slot Instead of picking a time yourself, pass `schedulingMode: QUEUE_NEXT` and leave `postAt` out. Postpone puts the post in the next open slot on the account's [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""}, skipping any slot that already has a post on it. ```json { "input": { "username": "myhandle", "schedulingMode": "QUEUE_NEXT", "caption": "This one goes out at my next open slot.", "media": [{ "url": "https://example.com/sunset.jpg" }], "submissions": [ { "mediaType": "FEED_POST" } ] } } ``` Leave `postAt` off the submissions — the resolved slot becomes the date for all of them. The account needs a schedule set up for this to work, `postAt` and `QUEUE_NEXT` can't be combined, and `updateScheduledInstagramPost` rejects `schedulingMode`. See [Scheduling at the Next Open Slot](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot) for the full rules. ### Carousel Post with Multiple Images Schedule a carousel post with multiple images from your Content Library. ```json { "input": { "username": "myhandle", "caption": "Behind the scenes from our latest photoshoot! Swipe to see more 📸✨", "media": [{ "id": "1231" }, { "id": "1233" }, { "id": "1235" }], "submissions": [ { "postAt": "2025-12-01T15:30:00Z", "mediaType": "CAROUSEL" } ] } } ``` ### Reel with Feed Sharing Schedule a Reel that will also appear in the main feed. ```json { "input": { "username": "myhandle", "caption": "Quick tutorial on our latest technique! Save this for later 💡 #tutorial #tips #howto", "media": [{ "name": "tutorial-video.mp4" }], "submissions": [ { "postAt": "2025-12-01T16:00:00Z", "mediaType": "REEL", "shareToFeed": true } ] } } ``` ### Instagram Story Schedule a temporary Story post. ```json { "input": { "username": "myhandle", "media": [{ "url": "https://example.com/story-image.jpg" }], "submissions": [ { "postAt": "2025-12-01T14:00:00Z", "mediaType": "STORY" } ] } } ``` ### Post with Collaborators and First Comment Schedule a post with tagged collaborators and an automatic first comment. ```json { "input": { "username": "myhandle", "caption": "Amazing collaboration with some incredible creators! 🤝", "firstComment": "Thanks to everyone who made this possible! Check out their profiles 👆", "collaborators": ["collaborator1", "collaborator2", "collaborator3"], "media": [{ "url": "https://example.com/collab-photo.jpg" }], "submissions": [ { "postAt": "2025-12-01T19:00:00Z", "mediaType": "FEED_POST" } ] } } ``` ## Response Types ### Success Response ```json { "data": { "scheduleInstagramPost": { "success": true, "errors": [], "post": { "id": "123", "socialAccount": { "id": "456", "username": "myhandle" }, "publishingStatus": "READY_TO_PUBLISH", "caption": "Beautiful sunset from our latest adventure! 🌅 #photography #sunset #travel", "firstComment": "", "collaborators": [], "gallery": { "id": "789", "media": [ { "id": "101", "url": "https://cdn.example.com/sunset.jpg", "type": "image" } ] }, "submissions": [ { "id": "112", "mediaType": "FEED_POST", "shareToFeed": false, "postAt": "2025-12-01T18:00:00Z", "publishingMethod": "AUTOMATIC" } ] } } } } ``` ### Error Response ```json { "data": { "scheduleInstagramPost": { "success": false, "errors": [ { "field": "caption", "message": "Instagram captions can have at most 30 hashtags, but yours has 35 (3-5 hashtags recommended)" } ], "post": null } } } ``` ## Updating a Post To update an existing scheduled post, use the same input type with the corresponding update mutation and include the post `id`: ```graphql [Mutation] mutation UpdateScheduledInstagramPost($input: ScheduleInstagramPostInput!) { updateScheduledInstagramPost(input: $input) { success errors { field message } post { id } } } ``` Pass the `id` of the post you want to update in the input, along with the fields you want to change: ```json { "input": { "id": "12345", "username": "myhandle", "caption": "Updated caption!", "submissions": [{"postAt": "2025-12-01T15:30:00Z", "mediaType": "FEED"}] } } ``` ::note The update mutation uses the same input type as the create mutation. Include the `id` field to specify which post to update. Only scheduled posts that have not been published yet can be updated. :: `postAt` is required on an update, so every update states the date the post goes out on — pass the date it already has to leave it where it is. A draft staying a draft is exempt, and a draft you're promoting to `READY_TO_PUBLISH` needs a date of your choosing; see [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). `schedulingMode` is not accepted on an update at all; see [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ## Validation Rules ::note The API enforces Instagram's content policies and technical limitations: :: - **Caption Length**: Maximum 2,200 characters - **Hashtags**: Maximum 30 hashtags per caption (3-5 recommended for better reach) - **Collaborators**: Maximum 3 collaborators per post - **Carousel Media**: 2-10 images/videos per carousel - **Reel Duration**: Minimum 3 seconds for video content - **Aspect Ratios**: Images must be between 0.558:1 and 1.92:1 (approximately) - **File Sizes**: Videos max 30MB, images max 8MB - **GIF Support**: GIFs are not supported on Instagram - **Audio**: Only on Reels, and only on accounts connected via Facebook. Volumes must be between 0 and 100 - **Story Limit**: Stories can only contain one image or video - **Scheduling**: Posts must be scheduled for future dates only - **Account Limits**: Respects your plan's monthly post limits ## Common Errors ::accordion :::accordion-item --- icon: i-lucide-alert-circle label: Instagram account is not connected to your Postpone account --- The specified username is not connected to your Postpone account. Connect the account in your settings first. ::: :::accordion-item --- icon: i-lucide-hash label: Instagram captions can have at most 30 hashtags --- Your caption contains too many hashtags. Instagram allows a maximum of 30 hashtags, but 3-5 hashtags are recommended for better engagement. ::: :::accordion-item --- icon: i-lucide-image label: Instagram post must have at least one image or video --- You must include media content. Use `media` to attach images or videos to your post. ::: :::accordion-item --- icon: i-lucide-file-x label: Instagram does not support GIFs --- GIF files cannot be posted to Instagram. Convert your GIF to MP4 video format instead. ::: :::accordion-item{icon="i-lucide-crop" label="Invalid aspect ratio"} Your image aspect ratio is outside Instagram's accepted range (0.558:1 to 1.92:1). Crop or resize your image to fit within these limits. ::: :::accordion-item{icon="i-lucide-video" label="Reels can only have one video"} Instagram Reels support only single videos. For multiple videos, use a carousel post instead. ::: :::accordion-item --- icon: i-lucide-clock label: Reels videos must be at least 3 seconds long --- Your video is too short. Instagram Reels require a minimum duration of 3 seconds. ::: :::accordion-item --- icon: i-lucide-layers label: Stories can only have one image or video --- Instagram Stories support only single media items. Use separate submissions for multiple Story posts. ::: :::accordion-item --- icon: i-lucide-images label: Instagram carousels can have no more than 10 images or videos --- You've included too many media items. Instagram carousels support a maximum of 10 images or videos. ::: :::accordion-item --- icon: i-lucide-calendar-x label: Post date cannot be in the past --- The `postAt` timestamp must be in the future. Check your timezone settings. ::: :::accordion-item --- icon: i-lucide-calendar-off label: Provide a post date, or use QUEUE_NEXT --- A new post has to say when it goes out: pass a `postAt`, or `schedulingMode: QUEUE_NEXT`. Drafts are exempt: leave `postAt` off one and it is parked on a placeholder date about 30 days out. ::: :::accordion-item{icon="i-lucide-calendar-off" label="Provide a post date"} `schedulingMode` isn't accepted on an update, so a `postAt` is the only way to say when the post goes out. You'll also see it when promoting a parked draft to `publishingStatus: READY_TO_PUBLISH` without giving it a date. ::: :::accordion-item --- icon: i-lucide-calendar-clock label: Account has no schedule set up --- `schedulingMode: QUEUE_NEXT` needs an [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""} on the account you're posting to. Set one up, or pass an explicit `postAt`. ::: :::accordion-item --- icon: i-lucide-pencil-off label: schedulingMode is not supported when updating a post --- `schedulingMode` is only accepted when scheduling a new post. To move a post that is already scheduled, pass a `postAt` to `updateScheduledInstagramPost`. ::: :::accordion-item{icon="i-lucide-music" label="Invalid Instagram audio"} The `audio.audioId` you passed isn't a valid Instagram audio ID. Use an `audioId` returned by [`instagramAudioSearch`](https://developers.postpone.app/#searching-for-audio). Reported on the `audio_id` field. ::: :::accordion-item --- icon: i-lucide-music-4 label: Audio can only be added on Instagram accounts connected with Facebook --- Instagram's Audio API is only available to accounts connected via Facebook. Reconnect the account through Facebook, or schedule the Reel without `audio`. Reported on the `audio_id` field. ::: :::accordion-item --- icon: i-lucide-volume-2 label: Ensure this value is less than or equal to 100 --- `audio.audioVolume` and `audio.videoVolume` must be between 0 and 100. ::: :::accordion-item --- icon: i-lucide-users label: Instagram posts can have at most 3 collaborators --- You've tagged too many collaborators. Instagram allows a maximum of 3 collaborators per post. ::: :::accordion-item --- icon: i-lucide-trending-up label: Post outside of plan limits --- You've reached your plan's Instagram post limit for the scheduled month. Upgrade your plan or schedule for a different month. ::: :: # Facebook ## Mutation Use the `scheduleFacebookPost` mutation to schedule posts to Facebook: ```graphql [Mutation] mutation ScheduleFacebookPost($input: ScheduleFacebookPostInput!) { scheduleFacebookPost(input: $input) { success errors { field message } post { id socialAccount { id username } publishingStatus text firstComment gallery { id galleryMediaSet { id media { url mimeType } } } submissions { id postAt mediaType publishingMethod } } } } ``` ## Input Parameters ### ScheduleFacebookPostInput ::field-group :::field{required name="username" type="String!"} The username of the connected Facebook page to post from ::: :::field{name="schedulingMode" type="SchedulingModeType"} How the post date is decided. Options: `SPECIFIC_TIME`, `QUEUE_NEXT` (default: `SPECIFIC_TIME`). Only accepted when scheduling a new post. See [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ::: :::field --- required: true name: submissions type: "[FacebookPostSubmissionInputType!]!" --- Array of submissions defining when and how to publish (minimum 1 submission) ::: :::field{name="text" type="String"} The main text content of the Facebook post ::: :::field{name="firstComment" type="String"} An optional first comment to add to the post after publishing ::: :::field --- name: media type: "[SimpleMediaInput]" --- Media files to attach to the post. Each item references a file by `id`, `name`, or `url`. See [Attaching Media](https://developers.postpone.app/scheduling-posts#attaching-media). Only supported when scheduling new posts, not when updating existing ones. ::: :::field{name="publishingStatus" type="PublishingStatusType"} Publishing status. Options: `READY_TO_PUBLISH`, `DRAFT` (default: `READY_TO_PUBLISH`) ::: :::field --- name: postTags type: "[ID!]" --- Array of post tag IDs to organize your content. See [Post Tags](https://developers.postpone.app/scheduling-posts/post-tags). ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ### FacebookPostSubmissionInputType Each submission in the array supports these parameters: ::field-group :::field{name="postAt" type="DateTime"} When to publish the post (must be in the future). Required unless you pass `schedulingMode: QUEUE_NEXT` on the post when scheduling a new one, or the post is a draft. See [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). ::: :::field{required name="mediaType" type="FacebookMediaType!"} Type of Facebook content. Options: `POST`, `REEL`, `STORY` ::: :::field{name="publishingMethod" type="PublishingMethodType"} How to publish. Options: `AUTOMATIC`, `MANUAL` (default: `AUTOMATIC`) ::: :::field{name="id" type="ID"} ID of existing submission when updating (only for updates) ::: :: ## Examples ### Simple Facebook Post Schedule a basic Facebook post with text only. ```json { "input": { "username": "myfacebookpage", "text": "Hello Facebook! This is our first scheduled post 🎉", "submissions": [ { "postAt": "2025-12-01T15:30:00Z", "mediaType": "POST" } ] } } ``` ### Scheduling at the Next Open Slot Instead of picking a time yourself, pass `schedulingMode: QUEUE_NEXT` and leave `postAt` out. Postpone puts the post in the next open slot on the account's [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""}, skipping any slot that already has a post on it. ```json { "input": { "username": "myfacebookpage", "schedulingMode": "QUEUE_NEXT", "text": "This one goes out at our next open slot.", "submissions": [ { "mediaType": "POST" } ] } } ``` Leave `postAt` off the submissions — the resolved slot becomes the date for all of them. The account needs a schedule set up for this to work, `postAt` and `QUEUE_NEXT` can't be combined, and `updateScheduledFacebookPost` rejects `schedulingMode`. See [Scheduling at the Next Open Slot](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot) for the full rules. ### Facebook Post with Media URL Schedule a Facebook post by uploading media from an external URL. The media will be automatically downloaded and added to your Content Library. ```json { "input": { "username": "myfacebookpage", "text": "Check out this amazing sunset! 🌅", "media": [{ "url": "https://example.com/sunset.jpg" }], "submissions": [ { "postAt": "2025-12-01T15:30:00Z", "mediaType": "POST" } ] } } ``` ### Facebook Post with Content Library Media Schedule a Facebook post using media already in your Content Library. Reference the file by its `id`, which every upload mutation returns. ```json { "input": { "username": "myfacebookpage", "text": "Sharing our company logo! 🏢", "media": [{ "id": "1042" }], "submissions": [ { "postAt": "2025-12-01T15:30:00Z", "mediaType": "POST" } ] } } ``` You can also reference the file by name. Names are matched case-insensitively and the first match wins, so prefer `id` when several files share a name. ```json { "media": [{ "name": "company-logo.png" }] } ``` ### Facebook Reel Schedule a Facebook Reel with video content. ```json { "input": { "username": "myfacebookpage", "text": "Behind the scenes at our office! 🎬", "media": [{ "url": "https://example.com/office-tour.mp4" }], "submissions": [ { "postAt": "2025-12-01T15:30:00Z", "mediaType": "REEL" } ] } } ``` ### Facebook Story Schedule a Facebook Story that will appear in your page's story highlights. ```json { "input": { "username": "myfacebookpage", "media": [{ "url": "https://example.com/story-image.jpg" }], "submissions": [ { "postAt": "2025-12-01T15:30:00Z", "mediaType": "STORY" } ] } } ``` ### Facebook Post with First Comment Schedule a Facebook post with an automatic first comment that will be posted after the main post is published. ```json { "input": { "username": "myfacebookpage", "text": "What do you think about our latest product update?", "firstComment": "Let us know in the comments below! We'd love to hear your feedback 💬", "submissions": [ { "postAt": "2025-12-01T15:30:00Z", "mediaType": "POST" } ] } } ``` ### Facebook Carousel Post Schedule a Facebook post with multiple images in a carousel format. ```json { "input": { "username": "myfacebookpage", "text": "Take a look at our product gallery! 📸", "media": [{ "id": "1234" }, { "id": "1236" }, { "id": "1238" }], "submissions": [ { "postAt": "2025-12-01T15:30:00Z", "mediaType": "POST" } ] } } ``` ### Manual Publishing Schedule a Facebook post for manual publishing, which will require you to manually approve the post in your dashboard before it goes live. ```json { "input": { "username": "myfacebookpage", "text": "This post requires manual approval before publishing.", "submissions": [ { "postAt": "2025-12-01T15:30:00Z", "mediaType": "POST", "publishingMethod": "MANUAL" } ] } } ``` ## Response Types ### Success Response ```json { "data": { "scheduleFacebookPost": { "success": true, "errors": [], "post": { "id": "123", "socialAccount": { "id": "456", "username": "myfacebookpage" }, "publishingStatus": "READY_TO_PUBLISH", "text": "Hello Facebook! This is our first scheduled post 🎉", "firstComment": null, "gallery": null, "submissions": [ { "id": "789", "postAt": "2025-12-01T15:30:00Z", "mediaType": "POST", "publishingMethod": "AUTOMATIC" } ] } } } } ``` ### Error Response ```json { "data": { "scheduleFacebookPost": { "success": false, "errors": [ { "field": "text", "message": "Facebook posts cannot exceed 63,206 characters." } ], "post": null } } } ``` ## Updating a Post To update an existing scheduled post, use the same input type with the corresponding update mutation and include the post `id`: ```graphql [Mutation] mutation UpdateScheduledFacebookPost($input: ScheduleFacebookPostInput!) { updateScheduledFacebookPost(input: $input) { success errors { field message } post { id } } } ``` Pass the `id` of the post you want to update in the input, along with the fields you want to change: ```json { "input": { "id": "12345", "username": "mypagename", "text": "Updated post text!", "submissions": [{"postAt": "2025-12-01T15:30:00Z"}] } } ``` ::note The update mutation uses the same input type as the create mutation. Include the `id` field to specify which post to update. Only scheduled posts that have not been published yet can be updated. :: `postAt` is required on an update, so every update states the date the post goes out on — pass the date it already has to leave it where it is. A draft staying a draft is exempt, and a draft you're promoting to `READY_TO_PUBLISH` needs a date of your choosing; see [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). `schedulingMode` is not accepted on an update at all; see [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ## Validation Rules ::note The API enforces Facebook's content policies and technical limitations: :: - **Text Length**: Maximum 63,206 characters for post text - **Media**: Maximum 10 images/videos per carousel post - **Video**: Maximum 4GB file size for regular posts - **Reels**: 3-90 seconds duration, minimum 960x540 resolution, 9:16 aspect ratio recommended - **Stories**: 3-60 seconds duration for video, 9:16 aspect ratio recommended - **Scheduling**: Posts must be scheduled for future dates only - **Account Limits**: Respects your plan's monthly post limits ## Media Type Guidelines ### POST - Standard Facebook feed posts - Supports text, images, videos, and carousels - Best for general content sharing and engagement ### REEL - Short-form vertical videos - 3-90 seconds duration - Minimum 960x540 resolution - 9:16 aspect ratio recommended for best results - Ideal for creative, engaging video content - Can use a custom cover image or a frame of the video as the cover, see [Video Covers](https://developers.postpone.app/scheduling-posts/video-covers) ### STORY - Temporary content that appears in story highlights - 3-60 seconds for video content - 9:16 aspect ratio recommended - Perfect for behind-the-scenes content and quick updates ## Common Errors ::accordion :::accordion-item --- icon: i-lucide-alert-circle label: Facebook page is not connected --- The specified username is not connected to your Postpone account. Connect the Facebook page in your settings first. ::: :::accordion-item --- icon: i-lucide-type label: Post text exceeds character limit --- Facebook posts have a maximum length of 63,206 characters. Consider shortening your text or breaking it into multiple posts. ::: :::accordion-item --- icon: i-lucide-calendar-x label: Post date cannot be in the past --- The `postAt` timestamp must be in the future. Check your timezone settings. ::: :::accordion-item --- icon: i-lucide-calendar-off label: Provide a post date, or use QUEUE_NEXT --- A new post has to say when it goes out: pass a `postAt`, or `schedulingMode: QUEUE_NEXT`. Drafts are exempt: leave `postAt` off one and it is parked on a placeholder date about 30 days out. ::: :::accordion-item{icon="i-lucide-calendar-off" label="Provide a post date"} `schedulingMode` isn't accepted on an update, so a `postAt` is the only way to say when the post goes out. You'll also see it when promoting a parked draft to `publishingStatus: READY_TO_PUBLISH` without giving it a date. ::: :::accordion-item --- icon: i-lucide-calendar-clock label: Account has no schedule set up --- `schedulingMode: QUEUE_NEXT` needs an [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""} on the account you're posting to. Set one up, or pass an explicit `postAt`. ::: :::accordion-item --- icon: i-lucide-pencil-off label: schedulingMode is not supported when updating a post --- `schedulingMode` is only accepted when scheduling a new post. To move a post that is already scheduled, pass a `postAt` to `updateScheduledFacebookPost`. ::: :::accordion-item --- icon: i-lucide-image-off label: Too many media files in carousel --- Facebook carousels can have no more than 10 images or videos. Reduce the number of media files in your gallery. ::: :::accordion-item --- icon: i-lucide-video-off label: Invalid video format for Reels --- Facebook Reels require vertical videos (9:16 aspect ratio) with a duration between 3-90 seconds and minimum resolution of 960x540. ::: :::accordion-item --- icon: i-lucide-trending-up label: Over plan limits for scheduled month --- You've reached your plan's Facebook post limit for the scheduled month. Upgrade your plan or schedule for a different month. ::: :::accordion-item{icon="i-lucide-file-x" label="Media file too large"} The media file exceeds Facebook's size limits. Videos must be under 4GB, and images should be optimized for web use. ::: :: # Threads ## Mutation Use the `scheduleThreadsPost` mutation to schedule posts to Threads: ```graphql [Mutation] mutation ScheduleThreadsPost($input: ScheduleThreadsPostInput!) { scheduleThreadsPost(input: $input) { success errors { field message } post { id socialAccount { id username } publishingStatus submissions { id text order postAt gallery { id galleryMediaSet { id media { id url mimeType } } } } } } } ``` ## Input Parameters ### ScheduleThreadsPostInput ::field-group :::field{required name="username" type="String!"} The username of the connected Threads account to post from ::: :::field{name="postAt" type="DateTime"} When to publish the post (must be in the future). Required unless you pass `schedulingMode: QUEUE_NEXT` when scheduling a new post, or the post is a draft. See [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). ::: :::field{name="schedulingMode" type="SchedulingModeType"} How the post date is decided. Options: `SPECIFIC_TIME`, `QUEUE_NEXT` (default: `SPECIFIC_TIME`). Only accepted when scheduling a new post. See [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ::: :::field --- required: true name: thread type: "[ThreadsPostSubmissionInputType!]!" --- Array of posts that make up the thread (minimum 1 post) ::: :::field{name="replySettings" type="ThreadsReplySettingsType"} Who can reply to the post. Options: `EVERYONE`, `MENTIONED_USERS`, `FOLLOWING` ::: :::field{name="isGhostPost" type="Boolean"} Whether to publish as a ghost post. Ghost posts are text-only and auto-archive after 24 hours. Cannot be combined with media, GIFs, polls, or multi-post threads. ::: :::field{name="publishingStatus" type="PublishingStatusType"} Publishing status. Options: `READY_TO_PUBLISH`, `DRAFT` (default: `READY_TO_PUBLISH`) ::: :::field --- name: postTags type: "[ID!]" --- Array of post tag IDs to organize your content. See [Post Tags](https://developers.postpone.app/scheduling-posts/post-tags). ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ### ThreadsPostSubmissionInputType Each post in the thread supports these parameters: ::field-group :::field{required name="text" type="String!"} The post text content (max 500 characters) ::: :::field{required name="order" type="Int!"} Position of this post in the thread (0-based) ::: :::field{name="poll" type="PollInputType"} Poll configuration (cannot be combined with media) ::: :::field --- name: media type: "[SimpleMediaInput]" --- Media files to attach to the post. Each item references a file by `id`, `name`, or `url`. See [Attaching Media](https://developers.postpone.app/scheduling-posts#attaching-media). Only supported when scheduling new posts, not when updating existing ones. ::: :::field{name="repostAtAmount" type="Int"} Number of time units before auto-retweeting ::: :::field{name="repostAtUnit" type="String"} Time unit for auto-repost. Options: `hour`, `day` ::: :::field{name="repostRepeatDays" type="Int"} How many days to repeat the auto-repost (max 365) ::: :::field{name="repostFromSocialAccount" type="SocialAccountInputType"} Account to repost from (must be connected to your Postpone account) ::: :::field{name="removeAtAmount" type="Int"} Number of time units before auto-deleting the post ::: :::field{name="removeAtUnit" type="String"} Time unit for auto-deletion. Options: `hour`, `day` ::: :::field{name="removeMinLikes" type="Int"} Minimum likes required to prevent auto-deletion ::: :::field{name="final" type="Boolean"} Whether this is the final post in the thread (default: false) ::: :::field{name="topicTag" type="String"} Custom Threads [topic tag](https://developers.facebook.com/docs/threads/posts#topic-tags){rel=""nofollow""} for the post (max 50 characters). Cannot contain periods (`.`) or ampersands (`&`). If omitted, the first hashtag in the text is used as the topic tag. Only the first post in a thread (`order: 0`) can have a topic tag — values set on later posts are ignored. ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ## Examples ### Simple Post Schedule a basic post with text only. ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Hello Threads! This is my first scheduled post 🧵", "order": 0 } ] } } ``` ### Scheduling at the Next Open Slot Instead of picking a time yourself, pass `schedulingMode: QUEUE_NEXT` and leave `postAt` out. Postpone puts the post in the next open slot on the account's [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""}, skipping any slot that already has a post on it. ```json { "input": { "username": "myhandle", "schedulingMode": "QUEUE_NEXT", "thread": [ { "text": "This one goes out at my next open slot.", "order": 0 } ] } } ``` The whole thread lands on that one slot, the same way an explicit `postAt` applies to every post in the thread. The account needs a schedule set up for this to work, `postAt` and `QUEUE_NEXT` can't be combined, and `updateScheduledThreadsPost` rejects `schedulingMode`. See [Scheduling at the Next Open Slot](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot) for the full rules. ### Post with Media URL Schedule a post by uploading media from an external URL. Use this when you have a direct link to an image, video, or GIF that you want to attach. The media will be automatically downloaded and added to your Content Library. ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Check out this amazing sunset! 🌅", "order": 0, "media": [{ "url": "https://example.com/sunset.jpg" }] } ] } } ``` ### Post with Content Library Media Schedule a post using media already in your Content Library. Use this when you want to reuse media you've previously uploaded. Reference the file by its `id`, which every upload mutation returns. ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Sharing our company logo! 🏢", "order": 0, "media": [{ "id": "1042" }] } ] } } ``` You can also reference the file by name. Names are matched case-insensitively and the first match wins, so prefer `id` when several files share a name. ```json { "media": [{ "name": "company-logo.png" }] } ``` ### Thread with Multiple Posts Schedule a multi-post thread. ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "replySettings": "FOLLOWING", "thread": [ { "text": "🧵 Thread about building great APIs (1/3)", "order": 0 }, { "text": "First, always design your API with the developer experience in mind. Clear documentation and consistent patterns make all the difference.", "order": 1 }, { "text": "Second, implement proper error handling and rate limiting. Your API consumers will thank you when things go wrong (and they will).", "order": 2, "final": true } ] } } ``` ### Post with Poll Schedule a post with an interactive poll. ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "What's your favorite programming language? 💻", "order": 0, "poll": { "choices": ["JavaScript", "Python", "Go", "Rust"], "durationMinutes": 1440 } } ] } } ``` ### Post with a Custom Topic Tag Schedule a post with an explicit Threads topic tag. The tag can include spaces and is independent of any hashtags in the text. If `topicTag` is omitted, the first hashtag in the post text is used instead. ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Breaking down the latest model release.", "order": 0, "topicTag": "AI Threads" } ] } } ``` ### Auto-Repost Configuration Schedule a post with automatic reposting. ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Don't miss our latest blog post about API best practices! 📖", "order": 0, "repostAtAmount": 2, "repostAtUnit": "hour", "repostRepeatDays": 3, "repostFromSocialAccount": { "username": "mycompany" } } ] } } ``` ### Multiple Media Attachments Schedule a post with multiple images or videos (up to 20 media files per post). ```json { "input": { "username": "myhandle", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Here's a collection of our best product shots! 📸", "order": 0, "media": [ { "id": "1231" }, { "id": "1234" }, { "id": "1250" } ] } ] } } ``` ## Response Types ### Success Response ```json { "data": { "scheduleThreadsPost": { "success": true, "errors": [], "post": { "id": "123", "socialAccount": { "id": "456", "username": "myhandle" }, "publishingStatus": "READY_TO_PUBLISH", "submissions": [ { "id": "789", "text": "Hello Threads! This is my first scheduled post 🧵", "order": 0, "postAt": "2025-12-01T15:30:00Z", "gallery": null, "poll": null } ] } } } } ``` ### Error Response ```json { "data": { "scheduleThreadsPost": { "success": false, "errors": [ { "field": "text", "message": "You cannot schedule a post longer than 500 characters." } ], "post": null } } } ``` ## Updating a Post To update an existing scheduled post, use the same input type with the corresponding update mutation and include the post `id`: ```graphql [Mutation] mutation UpdateScheduledThreadsPost($input: ScheduleThreadsPostInput!) { updateScheduledThreadsPost(input: $input) { success errors { field message } post { id } } } ``` Pass the `id` of the post you want to update in the input, along with the fields you want to change: ```json { "input": { "id": "12345", "username": "myhandle", "postAt": "2026-01-15T10:30:00Z", "thread": [{"text": "Updated thread text!", "order": 0}] } } ``` ::note The update mutation uses the same input type as the create mutation. Include the `id` field to specify which post to update. Only scheduled posts that have not been published yet can be updated. :: `postAt` is required on an update, so every update states the date the post goes out on — pass the date it already has to leave it where it is. A draft staying a draft is exempt, and a draft you're promoting to `READY_TO_PUBLISH` needs a date of your choosing; see [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). `schedulingMode` is not accepted on an update at all; see [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ## Validation Rules ::note The API enforces Threads' content policies and technical limitations: :: - **Text Length**: Maximum 500 characters per post - **Media**: Maximum 20 images/videos per post - **Image Size**: Maximum 8MB per image - **GIF Size**: Maximum 15MB per GIF - **Video Size**: Maximum 1GB per video file - **Video Duration**: Maximum 300 seconds (5 minutes) per video - **Polls**: 2-4 options, each max 25 characters. Cannot be combined with media attachments. - **Topic Tag**: Maximum 50 characters. Cannot contain periods (`.`) or ampersands (`&`). A leading `#` is allowed and will be stripped. - **Threads**: No hard limit on thread length - **Scheduling**: Posts must be scheduled for future dates only - **Account Limits**: Respects your plan's monthly post limits ## Common Errors ::accordion :::accordion-item --- icon: i-lucide-alert-circle label: Threads account is not connected to your Postpone account --- The specified username is not connected to your Postpone account. Connect the account in your settings first. ::: :::accordion-item{icon="i-lucide-type" label="Post longer than character limit"} The post text exceeds the maximum length of 500 characters. Consider breaking it into multiple posts in a thread. ::: :::accordion-item --- icon: i-lucide-calendar-x label: Post date cannot be in the past --- The `postAt` timestamp must be in the future. Check your timezone settings. ::: :::accordion-item --- icon: i-lucide-calendar-off label: Provide a post date, or use QUEUE_NEXT --- A new post has to say when it goes out: pass a `postAt`, or `schedulingMode: QUEUE_NEXT`. Drafts are exempt: leave `postAt` off one and it is parked on a placeholder date about 30 days out. ::: :::accordion-item{icon="i-lucide-calendar-off" label="Provide a post date"} `schedulingMode` isn't accepted on an update, so a `postAt` is the only way to say when the post goes out. You'll also see it when promoting a parked draft to `publishingStatus: READY_TO_PUBLISH` without giving it a date. ::: :::accordion-item --- icon: i-lucide-calendar-clock label: Account has no schedule set up --- `schedulingMode: QUEUE_NEXT` needs an [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""} on the account you're posting to. Set one up, or pass an explicit `postAt`. ::: :::accordion-item --- icon: i-lucide-pencil-off label: schedulingMode is not supported when updating a post --- `schedulingMode` is only accepted when scheduling a new post. To move a post that is already scheduled, pass a `postAt` to `updateScheduledThreadsPost`. ::: :::accordion-item --- icon: i-lucide-image-off label: Cannot add poll to post with media --- Threads doesn't allow polls on posts with images, videos, or GIFs. Choose either media or a poll. ::: :::accordion-item{icon="i-lucide-images" label="Too many media files"} You can attach a maximum of 20 media files per post. Remove some media or split into multiple posts. ::: :::accordion-item{icon="i-lucide-file-x" label="Media file too large"} Media files exceed size limits: 8MB for images, 15MB for GIFs, 1GB for videos. Compress your files or use smaller versions. ::: :::accordion-item{icon="i-lucide-video-off" label="Video too long"} Videos cannot be longer than 5 minutes (300 seconds). Trim your video to fit within the duration limit. ::: :::accordion-item --- icon: i-lucide-trending-up label: Post is over the plan limits for the scheduled month --- You've reached your plan's post limit for the scheduled month. Upgrade your plan or schedule for a different month. ::: :: # Bluesky ## Mutation Use the `scheduleBlueskyPost` mutation to schedule posts to Bluesky: ```graphql [Mutation] mutation ScheduleBlueskyPost($input: ScheduleBlueskyPostInput!) { scheduleBlueskyPost(input: $input) { success errors { field message } post { id socialAccount { id username } publishingStatus allowRepliesFrom submissions { id text order postAt contentWarning languages tags gallery { id galleryMediaSet { id media { id url mimeType } } } } } } } ``` ## Input Parameters ### ScheduleBlueskyPostInput ::field-group :::field{required name="username" type="String!"} The username of the connected Bluesky account to post from ::: :::field{name="postAt" type="DateTime"} When to publish the post (must be in the future). Required unless you pass `schedulingMode: QUEUE_NEXT` when scheduling a new post, or the post is a draft. See [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). ::: :::field{name="schedulingMode" type="SchedulingModeType"} How the post date is decided. Options: `SPECIFIC_TIME`, `QUEUE_NEXT` (default: `SPECIFIC_TIME`). Only accepted when scheduling a new post. See [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ::: :::field --- required: true name: thread type: "[BlueskyPostSubmissionInputType!]!" --- Array of posts that make up the thread (minimum 1 post) ::: :::field{name="allowRepliesFrom" type="BlueskyAllowRepliesFromType"} Who can reply to the post. Options: `EVERYBODY`, `MENTIONED_USERS`, `FOLLOWED_USERS`, `NOBODY` ::: :::field{name="publishingStatus" type="PublishingStatusType"} Publishing status. Options: `READY_TO_PUBLISH`, `DRAFT` (default: `READY_TO_PUBLISH`) ::: :::field --- name: postTags type: "[ID!]" --- Array of post tag IDs to organize your content. See [Post Tags](https://developers.postpone.app/scheduling-posts/post-tags). ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ### BlueskyPostSubmissionInputType Each post in the thread supports these parameters: ::field-group :::field{required name="text" type="String!"} The post text content (max 300 characters) ::: :::field{required name="order" type="Int!"} Position of this post in the thread (0-based) ::: :::field{name="contentWarning" type="BlueskyContentWarningType"} Content warning label. Options: `NONE`, `SUGGESTIVE`, `NUDITY`, `PORN`. Defaults to `NONE`. ::: :::field --- name: languages type: "[String!]" --- IETF BCP 47 language codes for the post content ::: :::field --- name: tags type: "[String!]" --- Additional hashtags or topic tags for the post ::: :::field --- name: media type: "[SimpleMediaInput]" --- Media files to attach to the post. Each item references a file by `id`, `name`, or `url`. See [Attaching Media](https://developers.postpone.app/scheduling-posts#attaching-media). Only supported when scheduling new posts, not when updating existing ones. ::: :::field{name="repostAtAmount" type="Int"} Number of time units before auto-reposting ::: :::field{name="repostAtUnit" type="String"} Time unit for auto-repost. Options: `hour`, `day` ::: :::field{name="repostRepeatDays" type="Int"} How many days to repeat the auto-repost (max 365) ::: :::field{name="repostFromSocialAccount" type="SocialAccountInputType"} Account to repost from (must be connected to your Postpone account) ::: :::field{name="removeAtAmount" type="Int"} Number of time units before auto-deleting the post ::: :::field{name="removeAtUnit" type="String"} Time unit for auto-deletion. Options: `hour`, `day` ::: :::field{name="removeMinLikes" type="Int"} Minimum likes required to prevent auto-deletion ::: :::field{name="final" type="Boolean"} Whether this is the final post in the thread (default: false) ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ## Examples ### Simple Post Schedule a basic post with text only. ```json { "input": { "username": "myhandle.bsky.social", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Hello Bluesky! This is my first scheduled post 🌤️", "order": 0, "contentWarning": "NONE" } ] } } ``` ### Scheduling at the Next Open Slot Instead of picking a time yourself, pass `schedulingMode: QUEUE_NEXT` and leave `postAt` out. Postpone puts the post in the next open slot on the account's [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""}, skipping any slot that already has a post on it. ```json { "input": { "username": "myhandle.bsky.social", "schedulingMode": "QUEUE_NEXT", "thread": [ { "text": "This one goes out at my next open slot.", "order": 0, "contentWarning": "NONE" } ] } } ``` The whole thread lands on that one slot, the same way an explicit `postAt` applies to every post in the thread. The account needs a schedule set up for this to work, `postAt` and `QUEUE_NEXT` can't be combined, and `updateScheduledBlueskyPost` rejects `schedulingMode`. See [Scheduling at the Next Open Slot](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot) for the full rules. ### Post with Media URL Schedule a post by uploading media from an external URL. The media will be automatically downloaded and added to your Content Library. ```json { "input": { "username": "myhandle.bsky.social", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Check out this amazing sunset! 🌅", "order": 0, "contentWarning": "NONE", "media": [{ "url": "https://example.com/sunset.jpg" }] } ] } } ``` ### Post with Content Library Media Schedule a post using media already in your Content Library. Reference the file by its `id`, which every upload mutation returns. ```json { "input": { "username": "myhandle.bsky.social", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Sharing our company logo! 🏢", "order": 0, "contentWarning": "NONE", "media": [{ "id": "1042" }] } ] } } ``` You can also reference the file by name. Names are matched case-insensitively and the first match wins, so prefer `id` when several files share a name. ```json { "media": [{ "name": "company-logo.png" }] } ``` ### Thread with Multiple Posts Schedule a multi-post thread with reply restrictions. ```json { "input": { "username": "myhandle.bsky.social", "postAt": "2025-12-01T15:30:00Z", "allowRepliesFrom": "FOLLOWED_USERS", "thread": [ { "text": "🧵 Thread about building great APIs (1/3)", "order": 0, "contentWarning": "NONE" }, { "text": "First, always design your API with the developer experience in mind. Clear documentation and consistent patterns make all the difference.", "order": 1, "contentWarning": "NONE" }, { "text": "Second, implement proper error handling and rate limiting. Your API consumers will thank you when things go wrong (and they will).", "order": 2, "contentWarning": "NONE", "final": true } ] } } ``` ### Post with Content Warning Schedule a post with appropriate content warning labeling. ```json { "input": { "username": "myhandle.bsky.social", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Artistic photography from my latest shoot 📸", "order": 0, "contentWarning": "SUGGESTIVE", "media": [{ "url": "https://example.com/photoshoot.jpg" }] } ] } } ``` ### Multi-Language Post Schedule a post with language tags for better discoverability. ```json { "input": { "username": "myhandle.bsky.social", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Hello world! / Hola mundo! / Bonjour le monde!", "order": 0, "contentWarning": "NONE", "languages": ["en", "es", "fr"], "tags": ["multilingual", "greetings"] } ] } } ``` ### Auto-Repost Configuration Schedule a post with automatic reposting. ```json { "input": { "username": "myhandle.bsky.social", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Don't miss our latest blog post about API best practices! 📖", "order": 0, "contentWarning": "NONE", "repostAtAmount": 2, "repostAtUnit": "hour", "repostRepeatDays": 3, "repostFromSocialAccount": { "username": "mycompany.bsky.social" } } ] } } ``` ## Response Types ### Success Response ```json { "data": { "scheduleBlueskyPost": { "success": true, "errors": [], "post": { "id": "123", "socialAccount": { "id": "456", "username": "myhandle.bsky.social" }, "publishingStatus": "READY_TO_PUBLISH", "allowRepliesFrom": "EVERYBODY", "submissions": [ { "id": "789", "text": "Hello Bluesky! This is my first scheduled post 🌤️", "order": 0, "postAt": "2025-12-01T15:30:00Z", "contentWarning": "NONE", "languages": [], "tags": [], "gallery": null } ] } } } } ``` ### Error Response ```json { "data": { "scheduleBlueskyPost": { "success": false, "errors": [ { "field": "text", "message": "Post text cannot exceed 300 characters." } ], "post": null } } } ``` ## Updating a Post To update an existing scheduled post, use the same input type with the corresponding update mutation and include the post `id`: ```graphql [Mutation] mutation UpdateScheduledBlueskyPost($input: ScheduleBlueskyPostInput!) { updateScheduledBlueskyPost(input: $input) { success errors { field message } post { id } } } ``` Pass the `id` of the post you want to update in the input, along with the fields you want to change: ```json { "input": { "id": "12345", "username": "myhandle.bsky.social", "postAt": "2026-01-15T10:30:00Z", "thread": [{"text": "Updated post text!", "order": 0, "contentWarning": "NONE"}] } } ``` ::note The update mutation uses the same input type as the create mutation. Include the `id` field to specify which post to update. Only scheduled posts that have not been published yet can be updated. :: `postAt` is required on an update, so every update states the date the post goes out on — pass the date it already has to leave it where it is. A draft staying a draft is exempt, and a draft you're promoting to `READY_TO_PUBLISH` needs a date of your choosing; see [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). `schedulingMode` is not accepted on an update at all; see [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ## Validation Rules ::note The API enforces Bluesky's content policies and technical limitations: :: - **Text Length**: Maximum 300 characters per post - **Media**: Supports images and videos with reasonable file size limits - **Content Warnings**: Required field for all posts, use `NONE` for general audiences - **Languages**: Use standard IETF BCP 47 language codes (e.g., `en`, `es`, `fr`) - **Threads**: No hard limit on thread length - **Scheduling**: Posts must be scheduled for future dates only - **Account Limits**: Respects your plan's monthly post limits ## Common Errors ::accordion :::accordion-item --- icon: i-lucide-alert-circle label: Bluesky account is not connected --- The specified username is not connected to your Postpone account. Connect the account in your settings first. ::: :::accordion-item{icon="i-lucide-type" label="Post longer than character limit"} The post text exceeds the 300 character limit. Consider breaking long posts into threads. ::: :::accordion-item --- icon: i-lucide-calendar-x label: Post date cannot be in the past --- The `postAt` timestamp must be in the future. Check your timezone settings. ::: :::accordion-item --- icon: i-lucide-calendar-off label: Provide a post date, or use QUEUE_NEXT --- A new post has to say when it goes out: pass a `postAt`, or `schedulingMode: QUEUE_NEXT`. Drafts are exempt: leave `postAt` off one and it is parked on a placeholder date about 30 days out. ::: :::accordion-item{icon="i-lucide-calendar-off" label="Provide a post date"} `schedulingMode` isn't accepted on an update, so a `postAt` is the only way to say when the post goes out. You'll also see it when promoting a parked draft to `publishingStatus: READY_TO_PUBLISH` without giving it a date. ::: :::accordion-item --- icon: i-lucide-calendar-clock label: Account has no schedule set up --- `schedulingMode: QUEUE_NEXT` needs an [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""} on the account you're posting to. Set one up, or pass an explicit `postAt`. ::: :::accordion-item --- icon: i-lucide-pencil-off label: schedulingMode is not supported when updating a post --- `schedulingMode` is only accepted when scheduling a new post. To move a post that is already scheduled, pass a `postAt` to `updateScheduledBlueskyPost`. ::: :::accordion-item{icon="i-lucide-shield-alert" label="Invalid content warning"} The `contentWarning` field defaults to `NONE`, which is intended for general audiences. Use appropriate labels for adult content. ::: :::accordion-item{icon="i-lucide-globe" label="Invalid language code"} Language codes must follow IETF BCP 47 standard (e.g., `en` for English, `es` for Spanish). ::: :::accordion-item --- icon: i-lucide-trending-up label: Over plan limits for scheduled month --- You've reached your plan's post limit for the scheduled month. Upgrade your plan or schedule for a different month. ::: :: # TikTok ## Mutation Use the `scheduleTikTokPost` mutation to schedule video content to TikTok: ```graphql [Mutation] mutation ScheduleTikTokPost($input: ScheduleTikTokPostInput!) { scheduleTikTokPost(input: $input) { success errors { field message } post { id socialAccount { id username } publishingStatus caption description privacyLevel allowUsersToComment allowUsersToDuet allowUsersToStitch isBrandContent isPaidPartnership isAiGeneratedContent gallery { id galleryMediaSet { id media { url mimeType } } } submissions { id postAt publishingMethod } } } } ``` ## Input Parameters ### ScheduleTikTokPostInput ::field-group :::field{required name="username" type="String!"} The username of the connected TikTok account to post from ::: :::field{name="schedulingMode" type="SchedulingModeType"} How the post date is decided. Options: `SPECIFIC_TIME`, `QUEUE_NEXT` (default: `SPECIFIC_TIME`). Only accepted when scheduling a new post. See [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ::: :::field{name="caption" type="String"} The caption text for the video (optional) ::: :::field{name="description" type="String"} The description text for the video (optional) ::: :::field --- name: media type: "[SimpleMediaInput]" --- Media files to attach to the post: a single video, or several images for a photo carousel. Each item references a file by `id`, `name`, or `url`. See [Attaching Media](https://developers.postpone.app/scheduling-posts#attaching-media). Only supported when scheduling new posts, not when updating existing ones. ::: :::field{required name="privacyLevel" type="TikTokPrivacyLevel!"} Privacy setting for the video. Options: `PUBLIC_TO_EVERYONE`, `FOLLOWER_OF_CREATOR`, `MUTUAL_FOLLOW_FRIENDS`, `SELF_ONLY` ::: :::field{required name="allowUsersToComment" type="Boolean!"} Whether users can comment on the video ::: :::field{required name="allowUsersToDuet" type="Boolean!"} Whether users can create duets with the video ::: :::field{required name="allowUsersToStitch" type="Boolean!"} Whether users can stitch with the video ::: :::field{name="autoAddMusic" type="Boolean"} Whether to automatically add music to the video (default: false) ::: :::field{required name="isBrandContent" type="Boolean!"} Whether this is branded content that needs disclosure ::: :::field{required name="isPaidPartnership" type="Boolean!"} Whether this is a paid partnership that needs disclosure ::: :::field{name="isAiGeneratedContent" type="Boolean"} Whether the content is AI-generated (default: false) ::: :::field --- required: true name: submissions type: "[TikTokPostSubmissionInputType!]!" --- Array of submissions defining when to post (minimum 1 submission) ::: :::field{name="publishingStatus" type="PublishingStatusType"} Publishing status. Options: `READY_TO_PUBLISH`, `DRAFT` (default: `READY_TO_PUBLISH`) ::: :::field --- name: postTags type: "[ID!]" --- Array of post tag IDs to organize your content. See [Post Tags](https://developers.postpone.app/scheduling-posts/post-tags). ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ### TikTokPostSubmissionInputType Each submission defines a specific posting schedule: ::field-group :::field{name="postAt" type="DateTime"} When to publish the video (must be in the future). Required unless you pass `schedulingMode: QUEUE_NEXT` on the post when scheduling a new one, or the post is a draft. See [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). ::: :::field{name="publishingMethod" type="PublishingMethodType"} How to publish the content. Options: `AUTOMATIC`, `MANUAL` (default: `AUTOMATIC`) ::: :::field{name="id" type="ID"} ID of existing submission when updating (only for updates) ::: :: ## Video Covers To choose which frame of the video TikTok uses as the cover, set the video's cover with [`setVideoCover`](https://developers.postpone.app/scheduling-posts/video-covers) and `offsetMs` before the post publishes. TikTok doesn't support custom cover images, so covers set with `imageUrl` or `imageMediaId` are ignored and TikTok uses the first frame. ## Examples ### Simple TikTok Video Schedule a basic TikTok video with a video from URL. ```json { "input": { "username": "myhandle", "caption": "Check out this amazing dance! 💃 #dance #viral #fyp", "media": [{ "url": "https://example.com/dance-video.mp4" }], "privacyLevel": "PUBLIC_TO_EVERYONE", "allowUsersToComment": true, "allowUsersToDuet": true, "allowUsersToStitch": false, "isBrandContent": false, "isPaidPartnership": false, "submissions": [ { "postAt": "2025-12-01T18:00:00Z" } ] } } ``` ### Scheduling at the Next Open Slot Instead of picking a time yourself, pass `schedulingMode: QUEUE_NEXT` and leave `postAt` out. Postpone puts the video in the next open slot on the account's [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""}, skipping any slot that already has a post on it. ```json { "input": { "username": "myhandle", "schedulingMode": "QUEUE_NEXT", "caption": "This one goes out at my next open slot. #fyp", "media": [{ "url": "https://example.com/dance-video.mp4" }], "privacyLevel": "PUBLIC_TO_EVERYONE", "submissions": [{}] } } ``` Leave `postAt` off the submissions — the resolved slot becomes the date for all of them. The account needs a schedule set up for this to work, `postAt` and `QUEUE_NEXT` can't be combined, and `updateScheduledTikTokPost` rejects `schedulingMode`. See [Scheduling at the Next Open Slot](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot) for the full rules. ### TikTok Video with Content Library Media Schedule a video using media from your Content Library. Reference the file by its `id`, which every upload mutation returns. ```json { "input": { "username": "myhandle", "caption": "Tutorial time! Save this for later 📚 #tutorial #howto #learn", "description": "Step-by-step guide to mastering this technique", "media": [{ "id": "1043" }], "privacyLevel": "PUBLIC_TO_EVERYONE", "allowUsersToComment": true, "allowUsersToDuet": false, "allowUsersToStitch": true, "autoAddMusic": true, "isBrandContent": false, "isPaidPartnership": false, "submissions": [ { "postAt": "2025-12-01T16:30:00Z" } ] } } ``` You can also reference the file by name. Names are matched case-insensitively and the first match wins, so prefer `id` when several files share a name. ```json { "media": [{ "name": "tutorial-video.mp4" }] } ``` ### Branded Content Video Schedule a TikTok video with brand content disclosures. ```json { "input": { "username": "myhandle", "caption": "Loving this new product! #sponsored #brandpartner #ad", "media": [{ "url": "https://example.com/brand-video.mp4" }], "privacyLevel": "PUBLIC_TO_EVERYONE", "allowUsersToComment": true, "allowUsersToDuet": true, "allowUsersToStitch": true, "isBrandContent": true, "isPaidPartnership": true, "isAiGeneratedContent": false, "submissions": [ { "postAt": "2025-12-01T20:00:00Z" } ] } } ``` ### AI-Generated Content Video Schedule a video created with AI tools. ```json { "input": { "username": "myhandle", "caption": "AI-generated content experiment! What do you think? 🤖 #ai #tech #future", "media": [{ "url": "https://example.com/ai-video.mp4" }], "privacyLevel": "PUBLIC_TO_EVERYONE", "allowUsersToComment": true, "allowUsersToDuet": false, "allowUsersToStitch": false, "isBrandContent": false, "isPaidPartnership": false, "isAiGeneratedContent": true, "submissions": [ { "postAt": "2025-12-01T14:00:00Z" } ] } } ``` ## Response Types ### Success Response ```json { "data": { "scheduleTikTokPost": { "success": true, "errors": [], "post": { "id": "123", "socialAccount": { "id": "456", "username": "myhandle" }, "publishingStatus": "READY_TO_PUBLISH", "caption": "Check out this amazing dance! 💃 #dance #viral #fyp", "description": null, "privacyLevel": "PUBLIC_TO_EVERYONE", "allowUsersToComment": true, "allowUsersToDuet": true, "allowUsersToStitch": false, "isBrandContent": false, "isPaidPartnership": false, "isAiGeneratedContent": false, "gallery": { "id": "789", "media": [ { "id": "101", "url": "https://cdn.example.com/dance-video.mp4", "type": "video" } ] }, "submissions": [ { "id": "112", "postAt": "2025-12-01T18:00:00Z", "publishingMethod": "AUTOMATIC" } ] } } } } ``` ### Error Response ```json { "data": { "scheduleTikTokPost": { "success": false, "errors": [ { "field": "media", "message": "TikTok videos must be between 3 seconds and 10 minutes long." } ], "post": null } } } ``` ## Updating a Post To update an existing scheduled post, use the same input type with the corresponding update mutation and include the post `id`: ```graphql [Mutation] mutation UpdateScheduledTikTokPost($input: ScheduleTikTokPostInput!) { updateScheduledTikTokPost(input: $input) { success errors { field message } post { id } } } ``` Pass the `id` of the post you want to update in the input, along with the fields you want to change: ```json { "input": { "id": "12345", "username": "myhandle", "caption": "Updated caption!", "submissions": [{"postAt": "2025-12-01T15:30:00Z"}] } } ``` ::note The update mutation uses the same input type as the create mutation. Include the `id` field to specify which post to update. Only scheduled posts that have not been published yet can be updated. :: `postAt` is required on an update, so every update states the date the post goes out on — pass the date it already has to leave it where it is. A draft staying a draft is exempt, and a draft you're promoting to `READY_TO_PUBLISH` needs a date of your choosing; see [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). `schedulingMode` is not accepted on an update at all; see [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ## Validation Rules ::note The API enforces TikTok's content policies and technical limitations: :: - **Video Duration**: Minimum 3 seconds, maximum 10 minutes - **File Size**: Maximum 287.6MB per video - **Video Format**: Supports MP4, MOV, MPEG, AVI, WMV, 3GPP, WEBM - **Aspect Ratios**: Vertical (9:16) recommended, also supports 1:1 and 16:9 - **Resolution**: Minimum 540x960, maximum 1080x1920 for vertical videos - **Frame Rate**: Minimum 23fps, maximum 60fps - **Audio**: Required for video content - **Caption Length**: No strict limit, but shorter captions perform better - **Hashtags**: Use 3-5 relevant hashtags for better discoverability - **Brand Content**: Must be properly disclosed if sponsored - **AI Content**: Must be labeled if AI-generated - **Scheduling**: Posts must be scheduled for future dates only - **Account Limits**: Respects your plan's monthly post limits ## Common Errors ::accordion :::accordion-item --- icon: i-lucide-alert-circle label: TikTok account is not connected --- The specified username is not connected to your Postpone account. Connect the account in your settings first. ::: :::accordion-item --- icon: i-lucide-clock label: TikTok videos must be between 3 seconds and 10 minutes long --- Your video duration is outside TikTok's accepted range. Trim your video to be between 3 seconds and 10 minutes. ::: :::accordion-item{icon="i-lucide-video" label="TikTok post must have a video"} You must include a video file. Use `media` to attach a video to your post. ::: :::accordion-item{icon="i-lucide-file-x" label="Invalid video format"} Your video format is not supported. Use MP4, MOV, MPEG, AVI, WMV, 3GPP, or WEBM format. ::: :::accordion-item --- icon: i-lucide-hard-drive label: Video file size exceeds limit --- Your video file is too large. TikTok supports videos up to 287.6MB in size. ::: :::accordion-item{icon="i-lucide-crop" label="Invalid aspect ratio"} Your video aspect ratio is not optimal. Use vertical (9:16), square (1:1), or horizontal (16:9) formats. ::: :::accordion-item --- icon: i-lucide-calendar-x label: Post date cannot be in the past --- The `postAt` timestamp must be in the future. Check your timezone settings. ::: :::accordion-item --- icon: i-lucide-calendar-off label: Provide a post date, or use QUEUE_NEXT --- A new post has to say when it goes out: pass a `postAt`, or `schedulingMode: QUEUE_NEXT`. Drafts are exempt: leave `postAt` off one and it is parked on a placeholder date about 30 days out. ::: :::accordion-item{icon="i-lucide-calendar-off" label="Provide a post date"} `schedulingMode` isn't accepted on an update, so a `postAt` is the only way to say when the post goes out. You'll also see it when promoting a parked draft to `publishingStatus: READY_TO_PUBLISH` without giving it a date. ::: :::accordion-item --- icon: i-lucide-calendar-clock label: Account has no schedule set up --- `schedulingMode: QUEUE_NEXT` needs an [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""} on the account you're posting to. Set one up, or pass an explicit `postAt`. ::: :::accordion-item --- icon: i-lucide-pencil-off label: schedulingMode is not supported when updating a post --- `schedulingMode` is only accepted when scheduling a new post. To move a post that is already scheduled, pass a `postAt` to `updateScheduledTikTokPost`. ::: :::accordion-item --- icon: i-lucide-badge-check label: Brand content must be properly disclosed --- When `isBrandContent` is true, ensure your content follows TikTok's branded content guidelines. ::: :::accordion-item --- icon: i-lucide-trending-up label: Over plan limits for scheduled month --- You've reached your plan's TikTok post limit for the scheduled month. Upgrade your plan or schedule for a different month. ::: :: # YouTube Shorts ## Mutation Use the `scheduleYouTubePost` mutation to schedule video content to YouTube: ```graphql [Mutation] mutation ScheduleYouTubePost($input: ScheduleYouTubePostInput!) { scheduleYouTubePost(input: $input) { success errors { field message } post { id socialAccount { id username } publishingStatus title description tags madeForKids containsSyntheticMedia privacyStatus gallery { id galleryMediaSet { id media { url mimeType } } } submissions { id postAt publishingMethod } } } } ``` ## Input Parameters ### ScheduleYouTubePostInput ::field-group :::field{required name="username" type="String!"} The username of the connected YouTube account to post from ::: :::field{name="schedulingMode" type="SchedulingModeType"} How the post date is decided. Options: `SPECIFIC_TIME`, `QUEUE_NEXT` (default: `SPECIFIC_TIME`). Only accepted when scheduling a new post. See [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ::: :::field{required name="title" type="String!"} The title of the video (max 100 characters) ::: :::field{name="description" type="String"} The description of the video (max 5,000 characters) ::: :::field --- name: tags type: "[String!]" --- Array of tags for the video (max 500 characters total) ::: :::field{required name="madeForKids" type="Boolean!"} Whether the content is made for kids (COPPA compliance) ::: :::field{name="containsSyntheticMedia" type="Boolean"} Whether the video contains synthetic or altered media (default: false) ::: :::field --- name: media type: "[SimpleMediaInput]" --- The video to publish, as a single-item list. The item references a file by `id`, `name`, or `url`. See [Attaching Media](https://developers.postpone.app/scheduling-posts#attaching-media). Only supported when scheduling new posts, not when updating existing ones. ::: :::field{required name="privacyStatus" type="YouTubePrivacyStatus!"} Privacy setting for the video. Options: `PUBLIC`, `UNLISTED`, `PRIVATE` ::: :::field --- required: true name: submissions type: "[YouTubePostSubmissionInputType!]!" --- Array of submissions defining when to post (minimum 1 submission) ::: :::field{name="publishingStatus" type="PublishingStatusType"} Publishing status. Options: `READY_TO_PUBLISH`, `DRAFT` (default: `READY_TO_PUBLISH`) ::: :::field --- name: postTags type: "[ID!]" --- Array of post tag IDs to organize your content. See [Post Tags](https://developers.postpone.app/scheduling-posts/post-tags). ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ### YouTubePostSubmissionInputType Each submission defines a specific posting schedule: ::field-group :::field{name="postAt" type="DateTime"} When to publish the video (must be in the future). Required unless you pass `schedulingMode: QUEUE_NEXT` on the post when scheduling a new one, or the post is a draft. See [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). ::: :::field{name="publishingMethod" type="PublishingMethodType"} How to publish the content. Options: `AUTOMATIC`, `MANUAL` (default: `AUTOMATIC`) ::: :::field{name="id" type="ID"} ID of existing submission when updating (only for updates) ::: :: ## Examples ### Simple YouTube Short Schedule a basic YouTube Short with a video from URL. ```json { "input": { "username": "mychannel", "title": "Amazing Dance Tutorial #Shorts", "description": "Learn this viral dance in just 60 seconds! Follow for more tutorials.\n\n#dance #tutorial #shorts #viral", "tags": ["dance", "tutorial", "shorts", "viral", "howto"], "media": [{ "url": "https://example.com/dance-short.mp4" }], "madeForKids": false, "privacyStatus": "PUBLIC", "submissions": [ { "postAt": "2025-12-01T18:00:00Z" } ] } } ``` ### Scheduling at the Next Open Slot Instead of picking a time yourself, pass `schedulingMode: QUEUE_NEXT` and leave `postAt` out. Postpone puts the video in the next open slot on the account's [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""}, skipping any slot that already has a post on it. ```json { "input": { "username": "mychannel", "schedulingMode": "QUEUE_NEXT", "title": "This one goes out at my next open slot #Shorts", "media": [{ "url": "https://example.com/dance-short.mp4" }], "madeForKids": false, "privacyStatus": "PUBLIC", "submissions": [{}] } } ``` Leave `postAt` off the submissions — the resolved slot becomes the date for all of them. The account needs a schedule set up for this to work, `postAt` and `QUEUE_NEXT` can't be combined, and `updateScheduledYouTubePost` rejects `schedulingMode`. See [Scheduling at the Next Open Slot](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot) for the full rules. ### YouTube Short with Content Library Media Schedule a video using media from your Content Library. Reference the file by its `id`, which every upload mutation returns. ```json { "input": { "username": "mychannel", "title": "Quick Cooking Hack You Need to Try!", "description": "This cooking hack will change your life! Save this for later and try it today.\n\nIngredients mentioned:\n- Ingredient 1\n- Ingredient 2\n\n#cooking #foodhacks #shorts #recipe", "tags": ["cooking", "food", "hacks", "recipe", "kitchen"], "media": [{ "id": "1044" }], "madeForKids": false, "privacyStatus": "PUBLIC", "submissions": [ { "postAt": "2025-12-01T16:30:00Z" } ] } } ``` You can also reference the file by name. Names are matched case-insensitively and the first match wins, so prefer `id` when several files share a name. ```json { "media": [{ "name": "cooking-hack-video.mp4" }] } ``` ### Kids Content Video Schedule a YouTube video appropriate for children. ```json { "input": { "username": "kidschannel", "title": "Fun Learning Song for Kids", "description": "Educational and fun song for children to learn colors and shapes!\n\nSafe content for kids with no ads or external links.", "tags": ["kids", "education", "learning", "songs", "colors"], "media": [{ "url": "https://example.com/kids-song.mp4" }], "madeForKids": true, "privacyStatus": "PUBLIC", "submissions": [ { "postAt": "2025-12-01T14:00:00Z" } ] } } ``` ### Synthetic Media Content Schedule a video containing AI-generated or altered content. ```json { "input": { "username": "techchannel", "title": "AI-Generated Video Experiment", "description": "Testing out the latest AI video generation tools. This video contains synthetic media created using AI.\n\n#ai #technology #synthetic #experiment", "tags": ["ai", "technology", "synthetic", "experiment", "future"], "media": [{ "url": "https://example.com/ai-generated-video.mp4" }], "madeForKids": false, "containsSyntheticMedia": true, "privacyStatus": "PUBLIC", "submissions": [ { "postAt": "2025-12-01T20:00:00Z" } ] } } ``` ### Unlisted Video Schedule an unlisted video that won't appear in search results. ```json { "input": { "username": "mychannel", "title": "Behind the Scenes - Unlisted", "description": "Exclusive behind-the-scenes content for our community. This video is unlisted and only accessible via direct link.", "tags": ["behind-the-scenes", "exclusive", "community"], "media": [{ "url": "https://example.com/bts-video.mp4" }], "madeForKids": false, "privacyStatus": "UNLISTED", "submissions": [ { "postAt": "2025-12-01T12:00:00Z" } ] } } ``` ## Response Types ### Success Response ```json { "data": { "scheduleYouTubePost": { "success": true, "errors": [], "post": { "id": "123", "socialAccount": { "id": "456", "username": "mychannel" }, "publishingStatus": "READY_TO_PUBLISH", "title": "Amazing Dance Tutorial #Shorts", "description": "Learn this viral dance in just 60 seconds! Follow for more tutorials.\n\n#dance #tutorial #shorts #viral", "tags": ["dance", "tutorial", "shorts", "viral", "howto"], "madeForKids": false, "containsSyntheticMedia": false, "privacyStatus": "PUBLIC", "gallery": { "id": "789", "media": [ { "id": "101", "url": "https://cdn.example.com/dance-short.mp4", "type": "video" } ] }, "submissions": [ { "id": "112", "postAt": "2025-12-01T18:00:00Z", "publishingMethod": "AUTOMATIC" } ] } } } } ``` ### Error Response ```json { "data": { "scheduleYouTubePost": { "success": false, "errors": [ { "field": "title", "message": "YouTube video titles cannot exceed 100 characters." } ], "post": null } } } ``` ## Updating a Post To update an existing scheduled post, use the same input type with the corresponding update mutation and include the post `id`: ```graphql [Mutation] mutation UpdateScheduledYouTubePost($input: ScheduleYouTubePostInput!) { updateScheduledYouTubePost(input: $input) { success errors { field message } post { id } } } ``` Pass the `id` of the post you want to update in the input, along with the fields you want to change: ```json { "input": { "id": "12345", "username": "mychannelname", "title": "Updated Video Title", "description": "Updated description", "submissions": [{"postAt": "2025-12-01T15:30:00Z"}] } } ``` ::note The update mutation uses the same input type as the create mutation. Include the `id` field to specify which post to update. Only scheduled posts that have not been published yet can be updated. :: `postAt` is required on an update, so every update states the date the post goes out on — pass the date it already has to leave it where it is. A draft staying a draft is exempt, and a draft you're promoting to `READY_TO_PUBLISH` needs a date of your choosing; see [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). `schedulingMode` is not accepted on an update at all; see [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ## Validation Rules ::note The API enforces YouTube's content policies and technical limitations: :: - **Video Duration**: Maximum 60 seconds for YouTube Shorts, up to 12 hours for regular videos - **File Size**: Maximum 256GB per video - **Video Format**: Supports MP4, MOV, AVI, WMV, FLV, 3GPP, WEBM - **Aspect Ratios**: Vertical (9:16) recommended for Shorts, also supports 16:9 and 1:1 - **Resolution**: Up to 4K (3840x2160) supported - **Frame Rate**: Up to 60fps supported - **Title Length**: Maximum 100 characters - **Description Length**: Maximum 5,000 characters - **Tags**: Maximum 500 characters total across all tags - **COPPA Compliance**: Must correctly identify content made for kids - **Synthetic Media**: Must be disclosed if content is AI-generated or altered - **Scheduling**: Posts must be scheduled for future dates only - **Account Limits**: Respects your plan's monthly post limits ## Common Errors ::accordion :::accordion-item --- icon: i-lucide-alert-circle label: YouTube account is not connected --- The specified username is not connected to your Postpone account. Connect the account in your settings first. ::: :::accordion-item --- icon: i-lucide-type label: YouTube video titles cannot exceed 100 characters --- Your video title is too long. Keep titles under 100 characters for better visibility and compliance. ::: :::accordion-item{icon="i-lucide-video" label="YouTube post must have a video"} You must include a video file. Use `media` to attach a video to your post. ::: :::accordion-item{icon="i-lucide-file-x" label="Invalid video format"} Your video format is not supported. Use MP4, MOV, AVI, WMV, FLV, 3GPP, or WEBM format. ::: :::accordion-item --- icon: i-lucide-hard-drive label: Video file size exceeds limit --- Your video file is too large. YouTube supports videos up to 256GB in size. ::: :::accordion-item --- icon: i-lucide-clock label: YouTube Shorts must be 60 seconds or less --- For YouTube Shorts, videos must be 60 seconds or shorter. Longer videos will be treated as regular YouTube videos. ::: :::accordion-item --- icon: i-lucide-align-left label: Description exceeds character limit --- Your video description is too long. Keep descriptions under 5,000 characters. ::: :::accordion-item{icon="i-lucide-tag" label="Tags exceed character limit"} Your tags exceed the 500 character limit. Reduce the number or length of your tags. ::: :::accordion-item{icon="i-lucide-shield" label="COPPA compliance required"} You must specify whether content is made for kids to comply with COPPA regulations. ::: :::accordion-item --- icon: i-lucide-calendar-x label: Post date cannot be in the past --- The `postAt` timestamp must be in the future. Check your timezone settings. ::: :::accordion-item --- icon: i-lucide-calendar-off label: Provide a post date, or use QUEUE_NEXT --- A new post has to say when it goes out: pass a `postAt`, or `schedulingMode: QUEUE_NEXT`. Drafts are exempt: leave `postAt` off one and it is parked on a placeholder date about 30 days out. ::: :::accordion-item{icon="i-lucide-calendar-off" label="Provide a post date"} `schedulingMode` isn't accepted on an update, so a `postAt` is the only way to say when the post goes out. You'll also see it when promoting a parked draft to `publishingStatus: READY_TO_PUBLISH` without giving it a date. ::: :::accordion-item --- icon: i-lucide-calendar-clock label: Account has no schedule set up --- `schedulingMode: QUEUE_NEXT` needs an [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""} on the account you're posting to. Set one up, or pass an explicit `postAt`. ::: :::accordion-item --- icon: i-lucide-pencil-off label: schedulingMode is not supported when updating a post --- `schedulingMode` is only accepted when scheduling a new post. To move a post that is already scheduled, pass a `postAt` to `updateScheduledYouTubePost`. ::: :::accordion-item --- icon: i-lucide-trending-up label: Over plan limits for scheduled month --- You've reached your plan's YouTube post limit for the scheduled month. Upgrade your plan or schedule for a different month. ::: :: # LinkedIn ## Mutation Use the `scheduleLinkedInPost` mutation to schedule professional content to LinkedIn: ```graphql [Mutation] mutation ScheduleLinkedInPost($input: ScheduleLinkedInPostInput!) { scheduleLinkedInPost(input: $input) { success errors { field message } post { id socialAccount { id username } publishingStatus text link visibility firstComment gallery { id galleryMediaSet { id media { url mimeType } } } submissions { id postAt } } } } ``` ## Input Parameters ### ScheduleLinkedInPostInput ::field-group :::field{required name="username" type="String!"} The username of the connected LinkedIn account to post from ::: :::field{name="schedulingMode" type="SchedulingModeType"} How the post date is decided. Options: `SPECIFIC_TIME`, `QUEUE_NEXT` (default: `SPECIFIC_TIME`). Only accepted when scheduling a new post. See [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ::: :::field{required name="text" type="String!"} The main text content of the post (max 3,000 characters) ::: :::field{name="link" type="String"} URL to share in the post (optional) ::: :::field{required name="visibility" type="VisibilityType!"} Post visibility setting. Options: `PUBLIC`, `CONNECTIONS`, `LOGGED_IN` ::: :::field{name="firstComment" type="String"} The first comment to be posted automatically with the post ::: :::field --- name: media type: "[SimpleMediaInput]" --- Media files to attach to the post. Each item references a file by `id`, `name`, or `url`. See [Attaching Media](https://developers.postpone.app/scheduling-posts#attaching-media). Only supported when scheduling new posts, not when updating existing ones. ::: :::field --- required: true name: submissions type: "[LinkedInPostSubmissionInputType!]!" --- Array of submissions defining when to post (minimum 1 submission) ::: :::field{name="publishingStatus" type="PublishingStatusType"} Publishing status. Options: `READY_TO_PUBLISH`, `DRAFT` (default: `READY_TO_PUBLISH`) ::: :::field --- name: postTags type: "[ID!]" --- Array of post tag IDs to organize your content. See [Post Tags](https://developers.postpone.app/scheduling-posts/post-tags). ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ### LinkedInPostSubmissionInputType Each submission defines a specific posting schedule: ::field-group :::field{name="postAt" type="DateTime"} When to publish the post (must be in the future). Required unless you pass `schedulingMode: QUEUE_NEXT` on the post when scheduling a new one, or the post is a draft. See [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). ::: :::field{name="id" type="ID"} ID of existing submission when updating (only for updates) ::: :: ## Video Covers To give a video post a custom cover, set the video's cover with [`setVideoCover`](https://developers.postpone.app/scheduling-posts/video-covers) before the post publishes. You can use your own image or a frame of the video. ## Examples ### Simple LinkedIn Post Schedule a basic LinkedIn post with text content. ```json { "input": { "username": "myprofile", "text": "Excited to share insights from our latest project! The key to success in digital transformation is focusing on people-first strategies that empower teams to embrace change.\n\n#DigitalTransformation #Leadership #Innovation", "visibility": "PUBLIC", "submissions": [ { "postAt": "2025-12-01T09:00:00Z" } ] } } ``` ### Scheduling at the Next Open Slot Instead of picking a time yourself, pass `schedulingMode: QUEUE_NEXT` and leave `postAt` out. Postpone puts the post in the next open slot on the account's [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""}, skipping any slot that already has a post on it. ```json { "input": { "username": "myprofile", "schedulingMode": "QUEUE_NEXT", "text": "This one goes out at my next open slot.", "visibility": "PUBLIC", "submissions": [{}] } } ``` Leave `postAt` off the submissions — the resolved slot becomes the date for all of them. The account needs a schedule set up for this to work, `postAt` and `QUEUE_NEXT` can't be combined, and `updateScheduledLinkedInPost` rejects `schedulingMode`. See [Scheduling at the Next Open Slot](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot) for the full rules. ### LinkedIn Post with Media Schedule a LinkedIn post with an image from URL. ```json { "input": { "username": "myprofile", "text": "Just wrapped up an incredible conference! Here are my key takeaways from the sessions on emerging technologies and their impact on business strategy.\n\n📊 Data-driven decision making is more crucial than ever\n🚀 AI adoption requires thoughtful change management\n🤝 Collaboration tools are reshaping remote work\n\nWhat trends are you seeing in your industry?\n\n#Conference2024 #Technology #BusinessStrategy", "media": [{ "url": "https://example.com/conference-photo.jpg" }], "visibility": "PUBLIC", "submissions": [ { "postAt": "2025-12-01T14:30:00Z" } ] } } ``` ### LinkedIn Post with Link Share Schedule a LinkedIn post sharing an article or resource. ```json { "input": { "username": "myprofile", "text": "Great article on the future of remote work! The insights about hybrid collaboration models really resonated with our team's experience.\n\nKey highlights:\n• Flexible work arrangements increase productivity by 25%\n• Virtual team building requires intentional effort\n• Technology should enable, not complicate workflows\n\nWhat's your take on the future of work? Share your thoughts below! 👇\n\n#RemoteWork #FutureOfWork #Productivity #TeamCollaboration", "link": "https://example.com/remote-work-article", "visibility": "PUBLIC", "submissions": [ { "postAt": "2025-12-01T11:15:00Z" } ] } } ``` ### LinkedIn Post with First Comment Schedule a LinkedIn post with an automatic first comment. ```json { "input": { "username": "myprofile", "text": "Thrilled to announce that our team has successfully completed the Q4 project ahead of schedule! 🎉\n\nThis achievement wouldn't have been possible without the dedication and innovative thinking of every team member.\n\n#TeamWork #ProjectSuccess #Innovation #Milestone", "firstComment": "Special thanks to @colleague1 @colleague2 and @colleague3 for their outstanding contributions to this project. Your expertise made all the difference!", "media": [{ "name": "team-celebration.jpg" }], "visibility": "PUBLIC", "submissions": [ { "postAt": "2025-12-01T16:00:00Z" } ] } } ``` ### Connections-Only Post Schedule a post visible only to your LinkedIn connections. ```json { "input": { "username": "myprofile", "text": "Personal update for my network: After 5 amazing years at TechCorp, I'm excited to start a new chapter as Senior Director at InnovateCo!\n\nLooking forward to bringing my passion for digital transformation to help drive growth in the fintech space.\n\nGrateful for all the connections and opportunities that have led me here. Let's stay in touch!\n\n#CareerUpdate #NewBeginnings #Grateful #FinTech", "visibility": "CONNECTIONS", "submissions": [ { "postAt": "2025-12-01T08:00:00Z" } ] } } ``` ## Response Types ### Success Response ```json { "data": { "scheduleLinkedInPost": { "success": true, "errors": [], "post": { "id": "123", "socialAccount": { "id": "456", "username": "myprofile" }, "publishingStatus": "READY_TO_PUBLISH", "text": "Excited to share insights from our latest project! The key to success in digital transformation is focusing on people-first strategies that empower teams to embrace change.\n\n#DigitalTransformation #Leadership #Innovation", "link": null, "visibility": "PUBLIC", "firstComment": null, "gallery": null, "submissions": [ { "id": "789", "postAt": "2025-12-01T09:00:00Z" } ] } } } } ``` ### Error Response ```json { "data": { "scheduleLinkedInPost": { "success": false, "errors": [ { "field": "text", "message": "LinkedIn posts cannot exceed 3,000 characters." } ], "post": null } } } ``` ## Updating a Post To update an existing scheduled post, use the same input type with the corresponding update mutation and include the post `id`: ```graphql [Mutation] mutation UpdateScheduledLinkedInPost($input: ScheduleLinkedInPostInput!) { updateScheduledLinkedInPost(input: $input) { success errors { field message } post { id } } } ``` Pass the `id` of the post you want to update in the input, along with the fields you want to change: ```json { "input": { "id": "12345", "username": "myhandle", "text": "Updated LinkedIn post!", "submissions": [{"postAt": "2025-12-01T15:30:00Z"}] } } ``` ::note The update mutation uses the same input type as the create mutation. Include the `id` field to specify which post to update. Only scheduled posts that have not been published yet can be updated. :: `postAt` is required on an update, so every update states the date the post goes out on — pass the date it already has to leave it where it is. A draft staying a draft is exempt, and a draft you're promoting to `READY_TO_PUBLISH` needs a date of your choosing; see [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). `schedulingMode` is not accepted on an update at all; see [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ## Validation Rules ::note The API enforces LinkedIn's content policies and professional standards: :: - **Text Length**: Maximum 3,000 characters per post - **Media**: Supports images (JPEG, PNG, GIF) and videos (MP4, MOV, WMV) - **Image Requirements**: Maximum 20MB, minimum 552x552 pixels - **Video Requirements**: Maximum 5GB, minimum 256x144 pixels, maximum 10 minutes - **Link Previews**: Automatically generated for valid URLs - **Professional Tone**: Content should maintain professional standards - **Hashtags**: Use relevant professional hashtags for better discoverability - **Mentions**: Can mention other LinkedIn users with @ symbol - **Scheduling**: Posts must be scheduled for future dates only - **Account Limits**: Respects your plan's monthly post limits ## Common Errors ::accordion :::accordion-item --- icon: i-lucide-alert-circle label: LinkedIn account is not connected --- The specified username is not connected to your Postpone account. Connect the account in your settings first. ::: :::accordion-item --- icon: i-lucide-type label: LinkedIn posts cannot exceed 3,000 characters --- Your post text is too long. LinkedIn allows a maximum of 3,000 characters per post. ::: :::accordion-item{icon="i-lucide-file-x" label="Invalid media format"} Your media format is not supported. Use JPEG, PNG, or GIF for images, and MP4, MOV, or WMV for videos. ::: :::accordion-item{icon="i-lucide-image" label="Image file size exceeds limit"} Your image file is too large. LinkedIn supports images up to 20MB in size. ::: :::accordion-item{icon="i-lucide-video" label="Video file size exceeds limit"} Your video file is too large. LinkedIn supports videos up to 5GB in size. ::: :::accordion-item{icon="i-lucide-clock" label="Video duration exceeds limit"} Your video is too long. LinkedIn supports videos up to 10 minutes in length. ::: :::accordion-item{icon="i-lucide-link" label="Invalid URL format"} The provided link is not a valid URL. Ensure your link starts with http\:// or https\://. ::: :::accordion-item --- icon: i-lucide-calendar-x label: Post date cannot be in the past --- The `postAt` timestamp must be in the future. Check your timezone settings. ::: :::accordion-item --- icon: i-lucide-calendar-off label: Provide a post date, or use QUEUE_NEXT --- A new post has to say when it goes out: pass a `postAt`, or `schedulingMode: QUEUE_NEXT`. Drafts are exempt: leave `postAt` off one and it is parked on a placeholder date about 30 days out. ::: :::accordion-item{icon="i-lucide-calendar-off" label="Provide a post date"} `schedulingMode` isn't accepted on an update, so a `postAt` is the only way to say when the post goes out. You'll also see it when promoting a parked draft to `publishingStatus: READY_TO_PUBLISH` without giving it a date. ::: :::accordion-item --- icon: i-lucide-calendar-clock label: Account has no schedule set up --- `schedulingMode: QUEUE_NEXT` needs an [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""} on the account you're posting to. Set one up, or pass an explicit `postAt`. ::: :::accordion-item --- icon: i-lucide-pencil-off label: schedulingMode is not supported when updating a post --- `schedulingMode` is only accepted when scheduling a new post. To move a post that is already scheduled, pass a `postAt` to `updateScheduledLinkedInPost`. ::: :::accordion-item --- icon: i-lucide-trending-up label: Over plan limits for scheduled month --- You've reached your plan's LinkedIn post limit for the scheduled month. Upgrade your plan or schedule for a different month. ::: :: # Pinterest ## Getting Board IDs Before scheduling Pinterest posts, you need to fetch the available boards for your Pinterest account. The `boardId` is required when scheduling pins. ### Query Pinterest Boards Use the `pinterestBoards` query to fetch all boards for a connected Pinterest account: ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Query"} ```graphql query pinterestBoards($socialAccountId: ID!) { pinterestBoards(socialAccountId: $socialAccountId) { id name privacy } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "socialAccountId": "your-social-account-id" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "pinterestBoards": [ { "id": "123456789", "name": "Recipes", "privacy": "PUBLIC" }, { "id": "987654321", "name": "Home Decor", "privacy": "PUBLIC" }, { "id": "456789123", "name": "Travel Inspiration", "privacy": "SECRET" } ] } } ``` :::: ::: :: ::note **Finding your Social Account ID**: Use the `socialAccounts` query to get the ID of your connected Pinterest account. Look for accounts where `platform` is `"PINTEREST"`. :: ## Scheduling Posts ### Mutation Use the `schedulePinterestPost` mutation to schedule pins to Pinterest boards: ```graphql [Mutation] mutation SchedulePinterestPost($input: SchedulePinterestPostInput!) { schedulePinterestPost(input: $input) { success errors { field message } post { id socialAccount { id username } publishingStatus title description link dominantColor note boardId gallery { id galleryMediaSet { id media { url mimeType } } } submissions { id postAt } } } } ``` ## Input Parameters ### SchedulePinterestPostInput ::field-group :::field{required name="username" type="String!"} The username of the connected Pinterest account to post from ::: :::field{name="schedulingMode" type="SchedulingModeType"} How the post date is decided. Options: `SPECIFIC_TIME`, `QUEUE_NEXT` (default: `SPECIFIC_TIME`). Only accepted when scheduling a new post. See [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ::: :::field{required name="title" type="String!"} The title of the pin (max 100 characters) ::: :::field{name="description" type="String"} The description of the pin (max 500 characters) ::: :::field{name="link" type="String"} URL that the pin should link to when clicked ::: :::field{name="dominantColor" type="String"} Hex color code representing the dominant color of the pin image ::: :::field{name="note" type="String"} Private note for the pin (only visible to you) ::: :::field{required name="boardId" type="String!"} The ID of the Pinterest board to pin to. Use the `pinterestBoards` query to fetch available boards and their IDs. ::: :::field --- name: media type: "[SimpleMediaInput]" --- The image to pin, as a single-item list. The item references a file by `id`, `name`, or `url`. See [Attaching Media](https://developers.postpone.app/scheduling-posts#attaching-media). Only supported when scheduling new posts, not when updating existing ones. ::: :::field --- required: true name: submissions type: "[PinterestPostSubmissionInputType!]!" --- Array of submissions defining when to post (minimum 1 submission) ::: :::field{name="publishingStatus" type="PublishingStatusType"} Publishing status. Options: `READY_TO_PUBLISH`, `DRAFT` (default: `READY_TO_PUBLISH`) ::: :::field --- name: postTags type: "[ID!]" --- Array of post tag IDs to organize your content. See [Post Tags](https://developers.postpone.app/scheduling-posts/post-tags). ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ### PinterestPostSubmissionInputType Each submission defines a specific posting schedule: ::field-group :::field{name="postAt" type="DateTime"} When to publish the pin (must be in the future). Required unless you pass `schedulingMode: QUEUE_NEXT` on the post when scheduling a new one, or the post is a draft. See [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). ::: :::field{name="id" type="ID"} ID of existing submission when updating (only for updates) ::: :: ## Video Covers To give a video Pin a custom cover, set the video's cover with [`setVideoCover`](https://developers.postpone.app/scheduling-posts/video-covers) before the Pin publishes. You can use your own image or a frame of the video. Without one, Postpone sends an automatically generated thumbnail of the video. ## Examples ### Simple Pinterest Pin Schedule a basic Pinterest pin with an image from URL. ```json { "input": { "username": "mybrand", "title": "Delicious Chocolate Chip Cookies Recipe", "description": "Easy homemade chocolate chip cookies that are crispy on the outside and chewy on the inside. Perfect for any occasion! Get the full recipe on our blog.", "link": "https://example.com/chocolate-chip-cookies-recipe", "media": [{ "url": "https://example.com/cookie-image.jpg" }], "boardId": "123456789", "submissions": [ { "postAt": "2025-12-01T15:00:00Z" } ] } } ``` ### Scheduling at the Next Open Slot Instead of picking a time yourself, pass `schedulingMode: QUEUE_NEXT` and leave `postAt` out. Postpone puts the pin in the next open slot on the account's [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""}, skipping any slot that already has a post on it. ```json { "input": { "username": "mybrand", "schedulingMode": "QUEUE_NEXT", "title": "Delicious Chocolate Chip Cookies Recipe", "description": "This one goes out at my next open slot.", "link": "https://example.com/chocolate-chip-cookies-recipe", "media": [{ "url": "https://example.com/cookie-image.jpg" }], "boardId": "123456789", "submissions": [{}] } } ``` Leave `postAt` off the submissions — the resolved slot becomes the date for all of them. The account needs a schedule set up for this to work, `postAt` and `QUEUE_NEXT` can't be combined, and `updateScheduledPinterestPost` rejects `schedulingMode`. See [Scheduling at the Next Open Slot](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot) for the full rules. ### Pinterest Pin with Content Library Media Schedule a pin using an image from your Content Library. Reference the file by its `id`, which every upload mutation returns. ```json { "input": { "username": "mybrand", "title": "10 Home Decor Ideas for Small Spaces", "description": "Transform your small space with these creative and budget-friendly home decor ideas. From storage solutions to color schemes, we've got you covered! #homedecor #smallspaces #interiordesign", "link": "https://example.com/small-space-decor-ideas", "media": [{ "id": "1046" }], "dominantColor": "#F4E4BC", "boardId": "987654321", "submissions": [ { "postAt": "2025-12-01T18:30:00Z" } ] } } ``` You can also reference the file by name. Names are matched case-insensitively and the first match wins, so prefer `id` when several files share a name. ```json { "media": [{ "name": "small-space-decor.jpg" }] } ``` ### Pinterest Pin with Board Section Schedule a pin to a specific section within a board. ```json { "input": { "username": "mybrand", "title": "Summer Wedding Centerpiece Ideas", "description": "Beautiful and elegant centerpiece ideas perfect for summer weddings. These floral arrangements will add the perfect touch to your special day.", "link": "https://example.com/summer-wedding-centerpieces", "media": [{ "url": "https://example.com/wedding-centerpiece.jpg" }], "boardId": "456789123", "boardSectionId": "789123456", "submissions": [ { "postAt": "2025-12-01T12:00:00Z" } ] } } ``` ### Pinterest Pin with Private Note Schedule a pin with a private note for internal organization. ```json { "input": { "username": "mybrand", "title": "Best Travel Destinations 2024", "description": "Discover the most amazing travel destinations for 2024! From hidden gems to popular hotspots, plan your next adventure with our comprehensive guide.", "link": "https://example.com/travel-destinations-2024", "media": [{ "url": "https://example.com/travel-destinations.jpg" }], "note": "Part of Q4 travel content campaign - scheduled for peak engagement time", "dominantColor": "#4A90E2", "boardId": "321654987", "submissions": [ { "postAt": "2025-12-01T20:00:00Z" } ] } } ``` ### Fashion Pinterest Pin Schedule a fashion-related pin with rich description and keywords. ```json { "input": { "username": "fashionbrand", "title": "Cozy Fall Outfit Ideas for 2024", "description": "Stay stylish and comfortable this fall with these cozy outfit ideas! Perfect combinations of sweaters, boots, and accessories for the season. #falloutfits #fashion2024 #cozystyle #autumn #ootd", "link": "https://example.com/fall-fashion-2024", "media": [{ "id": "1047" }], "dominantColor": "#D2691E", "boardId": "159753468", "submissions": [ { "postAt": "2025-12-01T16:45:00Z" } ] } } ``` ## Response Types ### Success Response ```json { "data": { "schedulePinterestPost": { "success": true, "errors": [], "post": { "id": "123", "socialAccount": { "id": "456", "username": "mybrand" }, "publishingStatus": "READY_TO_PUBLISH", "title": "Delicious Chocolate Chip Cookies Recipe", "description": "Easy homemade chocolate chip cookies that are crispy on the outside and chewy on the inside. Perfect for any occasion! Get the full recipe on our blog.", "link": "https://example.com/chocolate-chip-cookies-recipe", "dominantColor": null, "note": null, "boardId": "123456789", "boardSectionId": null, "gallery": { "id": "789", "media": [ { "id": "101", "url": "https://cdn.example.com/cookie-image.jpg", "type": "image" } ] }, "submissions": [ { "id": "112", "postAt": "2025-12-01T15:00:00Z" } ] } } } } ``` ### Error Response ```json { "data": { "schedulePinterestPost": { "success": false, "errors": [ { "field": "title", "message": "Pinterest pin titles cannot exceed 100 characters." } ], "post": null } } } ``` ## Updating a Post To update an existing scheduled post, use the same input type with the corresponding update mutation and include the post `id`: ```graphql [Mutation] mutation UpdateScheduledPinterestPost($input: SchedulePinterestPostInput!) { updateScheduledPinterestPost(input: $input) { success errors { field message } post { id } } } ``` Pass the `id` of the post you want to update in the input, along with the fields you want to change: ```json { "input": { "id": "12345", "username": "myhandle", "title": "Updated Pin Title", "submissions": [{"postAt": "2025-12-01T15:30:00Z"}] } } ``` ::note The update mutation uses the same input type as the create mutation. Include the `id` field to specify which post to update. Only scheduled posts that have not been published yet can be updated. :: `postAt` is required on an update, so every update states the date the post goes out on — pass the date it already has to leave it where it is. A draft staying a draft is exempt, and a draft you're promoting to `READY_TO_PUBLISH` needs a date of your choosing; see [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). `schedulingMode` is not accepted on an update at all; see [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ## Validation Rules ::note The API enforces Pinterest's content policies and technical requirements: :: - **Title Length**: Maximum 100 characters - **Description Length**: Maximum 500 characters - **Image Requirements**: JPEG, PNG, or GIF format - **Image Size**: Minimum 600x900 pixels, maximum 1000x1500 pixels recommended - **Aspect Ratio**: 2:3 aspect ratio performs best (e.g., 1000x1500) - **File Size**: Maximum 20MB per image - **Link Requirements**: Must be a valid, accessible URL - **Board Access**: Must have write access to the specified board - **Content Quality**: Images should be high-quality and relevant to the title/description - **Scheduling**: Posts must be scheduled for future dates only - **Account Limits**: Respects your plan's monthly pin limits ## Common Errors ::accordion :::accordion-item --- icon: i-lucide-alert-circle label: Pinterest account is not connected --- The specified username is not connected to your Postpone account. Connect the account in your settings first. ::: :::accordion-item --- icon: i-lucide-type label: Pinterest pin titles cannot exceed 100 characters --- Your pin title is too long. Keep titles under 100 characters for optimal display. ::: :::accordion-item --- icon: i-lucide-image label: Pinterest pin must have an image --- You must include an image. Use `media` to attach an image to your pin. ::: :::accordion-item{icon="i-lucide-file-x" label="Invalid image format"} Your image format is not supported. Use JPEG, PNG, or GIF format for Pinterest pins. ::: :::accordion-item --- icon: i-lucide-hard-drive label: Image file size exceeds limit --- Your image file is too large. Pinterest supports images up to 20MB in size. ::: :::accordion-item{icon="i-lucide-crop" label="Image dimensions too small"} Your image is too small. Pinterest requires images to be at least 600x900 pixels for optimal performance. ::: :::accordion-item{icon="i-lucide-lock" label="Board not found or access denied"} The specified board ID is invalid or you don't have write access to this board. Check your board permissions. ::: :::accordion-item{icon="i-lucide-folder-x" label="Board section not found"} The specified board section ID doesn't exist or isn't accessible. Verify the section ID is correct. ::: :::accordion-item{icon="i-lucide-link" label="Invalid URL format"} The provided link is not a valid URL. Ensure your link starts with http\:// or https\://. ::: :::accordion-item --- icon: i-lucide-align-left label: Description exceeds character limit --- Your pin description is too long. Keep descriptions under 500 characters. ::: :::accordion-item --- icon: i-lucide-calendar-x label: Post date cannot be in the past --- The `postAt` timestamp must be in the future. Check your timezone settings. ::: :::accordion-item --- icon: i-lucide-calendar-off label: Provide a post date, or use QUEUE_NEXT --- A new post has to say when it goes out: pass a `postAt`, or `schedulingMode: QUEUE_NEXT`. Drafts are exempt: leave `postAt` off one and it is parked on a placeholder date about 30 days out. ::: :::accordion-item{icon="i-lucide-calendar-off" label="Provide a post date"} `schedulingMode` isn't accepted on an update, so a `postAt` is the only way to say when the post goes out. You'll also see it when promoting a parked draft to `publishingStatus: READY_TO_PUBLISH` without giving it a date. ::: :::accordion-item --- icon: i-lucide-calendar-clock label: Account has no schedule set up --- `schedulingMode: QUEUE_NEXT` needs an [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""} on the account you're posting to. Set one up, or pass an explicit `postAt`. ::: :::accordion-item --- icon: i-lucide-pencil-off label: schedulingMode is not supported when updating a post --- `schedulingMode` is only accepted when scheduling a new post. To move a post that is already scheduled, pass a `postAt` to `updateScheduledPinterestPost`. ::: :::accordion-item --- icon: i-lucide-trending-up label: Over plan limits for scheduled month --- You've reached your plan's Pinterest pin limit for the scheduled month. Upgrade your plan or schedule for a different month. ::: :: # Mastodon ## Mutation Use the `scheduleMastodonPost` mutation to schedule posts to Mastodon: ```graphql [Mutation] mutation ScheduleMastodonPost($input: ScheduleMastodonPostInput!) { scheduleMastodonPost(input: $input) { success errors { field message } post { id socialAccount { id username } publishingStatus submissions { id text order postAt visibility contentWarning language gallery { id galleryMediaSet { id media { id url mimeType } } } } } } } ``` ## Input Parameters ### ScheduleMastodonPostInput ::field-group :::field{required name="username" type="String!"} The username of the connected Mastodon account to post from ::: :::field{name="postAt" type="DateTime"} When to publish the post (must be in the future). Required unless you pass `schedulingMode: QUEUE_NEXT` when scheduling a new post, or the post is a draft. See [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). ::: :::field{name="schedulingMode" type="SchedulingModeType"} How the post date is decided. Options: `SPECIFIC_TIME`, `QUEUE_NEXT` (default: `SPECIFIC_TIME`). Only accepted when scheduling a new post. See [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ::: :::field --- required: true name: thread type: "[MastodonPostSubmissionInputType!]!" --- Array of posts that make up the thread (minimum 1 post) ::: :::field{name="publishingStatus" type="PublishingStatusType"} Publishing status. Options: `READY_TO_PUBLISH`, `DRAFT` (default: `READY_TO_PUBLISH`) ::: :::field --- name: postTags type: "[ID!]" --- Array of post tag IDs to organize your content. See [Post Tags](https://developers.postpone.app/scheduling-posts/post-tags). ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ### MastodonPostSubmissionInputType Each post in the thread supports these parameters: ::field-group :::field{required name="text" type="String!"} The post text content (max 500 characters by default, varies by server) ::: :::field{required name="order" type="Int!"} Position of this post in the thread (0-based) ::: :::field{required name="visibility" type="String!"} Post visibility. Options: `PUBLIC`, `UNLISTED`, `PRIVATE`, `DIRECT` ::: :::field{name="contentWarning" type="String"} Content warning text (also called spoiler text) ::: :::field{name="language" type="String"} ISO 639 language code for the post content ::: :::field --- name: media type: "[SimpleMediaInput]" --- Media files to attach to the post. Each item references a file by `id`, `name`, or `url`. See [Attaching Media](https://developers.postpone.app/scheduling-posts#attaching-media). Only supported when scheduling new posts, not when updating existing ones. ::: :::field{name="repostAtAmount" type="Int"} Number of time units before auto-boosting ::: :::field{name="repostAtUnit" type="String"} Time unit for auto-boost. Options: `hour`, `day` ::: :::field{name="repostRepeatDays" type="Int"} How many days to repeat the auto-boost (max 365) ::: :::field{name="repostFromSocialAccount" type="SocialAccountInputType"} Account to boost from (must be connected to your Postpone account) ::: :::field{name="removeAtAmount" type="Int"} Number of time units before auto-deleting the post ::: :::field{name="removeAtUnit" type="String"} Time unit for auto-deletion. Options: `hour`, `day` ::: :::field{name="removeMinLikes" type="Int"} Minimum favorites required to prevent auto-deletion ::: :::field{name="final" type="Boolean"} Whether this is the final post in the thread (default: false) ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ## Examples ### Simple Post Schedule a basic post with text only. ```json { "input": { "username": "myhandle@mastodon.social", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Hello Fediverse! This is my first scheduled post 🚀", "order": 0, "visibility": "PUBLIC" } ] } } ``` ### Scheduling at the Next Open Slot Instead of picking a time yourself, pass `schedulingMode: QUEUE_NEXT` and leave `postAt` out. Postpone puts the post in the next open slot on the account's [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""}, skipping any slot that already has a post on it. ```json { "input": { "username": "myhandle@mastodon.social", "schedulingMode": "QUEUE_NEXT", "thread": [ { "text": "This one goes out at my next open slot.", "order": 0, "visibility": "PUBLIC" } ] } } ``` The whole thread lands on that one slot, the same way an explicit `postAt` applies to every post in the thread. The account needs a schedule set up for this to work, `postAt` and `QUEUE_NEXT` can't be combined, and `updateScheduledMastodonPost` rejects `schedulingMode`. See [Scheduling at the Next Open Slot](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot) for the full rules. ### Post with Media URL Schedule a post by uploading media from an external URL. The media will be automatically downloaded and added to your Content Library. ```json { "input": { "username": "myhandle@mastodon.social", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Check out this amazing sunset! 🌅", "order": 0, "visibility": "PUBLIC", "media": [{ "url": "https://example.com/sunset.jpg" }] } ] } } ``` ### Post with Content Library Media Schedule a post using media already in your Content Library. Reference the file by its `id`, which every upload mutation returns. ```json { "input": { "username": "myhandle@mastodon.social", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Sharing our company logo! 🏢", "order": 0, "visibility": "PUBLIC", "media": [{ "id": "1042" }] } ] } } ``` You can also reference the file by name. Names are matched case-insensitively and the first match wins, so prefer `id` when several files share a name. ```json { "media": [{ "name": "company-logo.png" }] } ``` ### Thread with Multiple Posts Schedule a multi-post thread. ```json { "input": { "username": "myhandle@mastodon.social", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "🧵 Thread about building great APIs (1/3)", "order": 0, "visibility": "PUBLIC" }, { "text": "First, always design your API with the developer experience in mind. Clear documentation and consistent patterns make all the difference.", "order": 1, "visibility": "PUBLIC" }, { "text": "Second, implement proper error handling and rate limiting. Your API consumers will thank you when things go wrong (and they will).", "order": 2, "visibility": "PUBLIC", "final": true } ] } } ``` ### Post with Content Warning Schedule a post with a content warning (spoiler text). ```json { "input": { "username": "myhandle@mastodon.social", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "This is some sensitive content that might need a warning.", "order": 0, "visibility": "PUBLIC", "contentWarning": "Sensitive topic discussion" } ] } } ``` ### Unlisted Post with Language Schedule an unlisted post with a specific language. ```json { "input": { "username": "myhandle@mastodon.social", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Bonjour le monde! Ceci est un post en français.", "order": 0, "visibility": "UNLISTED", "language": "fr" } ] } } ``` ### Auto-Boost Configuration Schedule a post with automatic boosting. ```json { "input": { "username": "myhandle@mastodon.social", "postAt": "2025-12-01T15:30:00Z", "thread": [ { "text": "Don't miss our latest blog post about API best practices! 📖", "order": 0, "visibility": "PUBLIC", "repostAtAmount": 2, "repostAtUnit": "hour", "repostRepeatDays": 3, "repostFromSocialAccount": { "username": "mycompany@mastodon.social" } } ] } } ``` ## Response Types ### Success Response ```json { "data": { "scheduleMastodonPost": { "success": true, "errors": [], "post": { "id": "123", "socialAccount": { "id": "456", "username": "myhandle@mastodon.social" }, "publishingStatus": "READY_TO_PUBLISH", "submissions": [ { "id": "789", "text": "Hello Fediverse! This is my first scheduled post 🚀", "order": 0, "postAt": "2025-12-01T15:30:00Z", "visibility": "PUBLIC", "contentWarning": "", "language": "", "gallery": null } ] } } } } ``` ### Error Response ```json { "data": { "scheduleMastodonPost": { "success": false, "errors": [ { "field": "text", "message": "You cannot schedule a post longer than 500 characters." } ], "post": null } } } ``` ## Updating a Post To update an existing scheduled post, use the same input type with the corresponding update mutation and include the post `id`: ```graphql [Mutation] mutation UpdateScheduledMastodonPost($input: ScheduleMastodonPostInput!) { updateScheduledMastodonPost(input: $input) { success errors { field message } post { id } } } ``` Pass the `id` of the post you want to update in the input, along with the fields you want to change: ```json { "input": { "id": "12345", "username": "myhandle@mastodon.social", "postAt": "2026-01-15T10:30:00Z", "thread": [{"text": "Updated toot text!", "order": 0}] } } ``` ::note The update mutation uses the same input type as the create mutation. Include the `id` field to specify which post to update. Only scheduled posts that have not been published yet can be updated. :: `postAt` is required on an update, so every update states the date the post goes out on — pass the date it already has to leave it where it is. A draft staying a draft is exempt, and a draft you're promoting to `READY_TO_PUBLISH` needs a date of your choosing; see [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). `schedulingMode` is not accepted on an update at all; see [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ## Validation Rules ::note The API enforces Mastodon's content policies and technical limitations: :: - **Text Length**: 500 characters by default (varies by server), includes content warning text - **Media**: Maximum 20 images per post, 1 video or GIF per post - **Images**: PNG, JPG, HEIF, WEBP formats, maximum 16MB each - **Videos**: MP4, M4V, MOV, WebM formats, maximum 99MB, max frame rate 120fps - **GIFs**: Automatically converted to soundless MP4s, maximum 16MB - **Threads**: No hard limit on thread length - **Scheduling**: Posts must be scheduled for future dates only - **Visibility**: Must be one of PUBLIC, UNLISTED, PRIVATE, or DIRECT - **Language**: Must use valid ISO 639 language codes - **Account Limits**: Respects your plan's monthly post limits ## Visibility Options ::field-group :::field{name="PUBLIC" type="String"} Visible to everyone and appears in public timelines ::: :::field{name="UNLISTED" type="String"} Visible to everyone but doesn't appear in public timelines ::: :::field{name="PRIVATE" type="String"} Only visible to followers (followers-only) ::: :::field{name="DIRECT" type="String"} Only visible to mentioned users (direct message) ::: :: ## Common Errors ::accordion :::accordion-item --- icon: i-lucide-alert-circle label: Mastodon account is not connected --- The specified username is not connected to your Postpone account. Connect the account in your settings first. ::: :::accordion-item{icon="i-lucide-type" label="Post longer than character limit"} The post text (including content warning) exceeds the server's character limit. Most Mastodon servers allow 500 characters. ::: :::accordion-item --- icon: i-lucide-calendar-x label: Post date cannot be in the past --- The `postAt` timestamp must be in the future. Check your timezone settings. ::: :::accordion-item --- icon: i-lucide-calendar-off label: Provide a post date, or use QUEUE_NEXT --- A new post has to say when it goes out: pass a `postAt`, or `schedulingMode: QUEUE_NEXT`. Drafts are exempt: leave `postAt` off one and it is parked on a placeholder date about 30 days out. ::: :::accordion-item{icon="i-lucide-calendar-off" label="Provide a post date"} `schedulingMode` isn't accepted on an update, so a `postAt` is the only way to say when the post goes out. You'll also see it when promoting a parked draft to `publishingStatus: READY_TO_PUBLISH` without giving it a date. ::: :::accordion-item --- icon: i-lucide-calendar-clock label: Account has no schedule set up --- `schedulingMode: QUEUE_NEXT` needs an [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""} on the account you're posting to. Set one up, or pass an explicit `postAt`. ::: :::accordion-item --- icon: i-lucide-pencil-off label: schedulingMode is not supported when updating a post --- `schedulingMode` is only accepted when scheduling a new post. To move a post that is already scheduled, pass a `postAt` to `updateScheduledMastodonPost`. ::: :::accordion-item{icon="i-lucide-eye-off" label="Invalid visibility option"} The visibility must be one of: PUBLIC, UNLISTED, PRIVATE, or DIRECT. ::: :::accordion-item{icon="i-lucide-globe" label="Invalid language code"} The language code must be a valid ISO 639 language code (e.g., "en", "fr", "de"). ::: :::accordion-item{icon="i-lucide-image-off" label="Too many media attachments"} Mastodon posts can have a maximum of 20 images or 1 video/GIF. Reduce the number of media attachments. ::: :::accordion-item{icon="i-lucide-file-x" label="Media file too large"} Images must be under 16MB and videos under 99MB. Compress your media files or use smaller versions. ::: :::accordion-item --- icon: i-lucide-trending-up label: Over plan limits for scheduled month --- You've reached your plan's post limit for the scheduled month. Upgrade your plan or schedule for a different month. ::: :: # Tumblr ## Mutation Use the `scheduleTumblrPost` mutation to schedule content to Tumblr blogs: ```graphql [Mutation] mutation ScheduleTumblrPost($input: ScheduleTumblrPostInput!) { scheduleTumblrPost(input: $input) { success errors { field message } post { id socialAccount { id username } publishingStatus content layout tags communityLabel sourceUrl gallery { id galleryMediaSet { id media { url mimeType } } } submissions { id postAt } } } } ``` ## Input Parameters ### ScheduleTumblrPostInput ::field-group :::field{required name="username" type="String!"} The username of the connected Tumblr blog to post from ::: :::field{name="schedulingMode" type="SchedulingModeType"} How the post date is decided. Options: `SPECIFIC_TIME`, `QUEUE_NEXT` (default: `SPECIFIC_TIME`). Only accepted when scheduling a new post. See [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ::: :::field{required name="content" type="JSONString!"} The post content in Tumblr's NPF (Neue Post Format) structure ::: :::field{required name="layout" type="JSONString!"} The post layout configuration in NPF format ::: :::field --- name: tags type: "[String!]" --- Array of tags for the post (max 30 tags) ::: :::field{name="sourceUrl" type="String"} Source URL to attribute the content to (optional) ::: :::field{required name="communityLabel" type="TumblrCommunityLabelType!"} Community labeling for content. Options: `FOR_EVERYONE`, `MATURE`, `DRUGS`, `VIOLENCE`, `SEXUAL_THEMES` ::: :::field --- required: true name: submissions type: "[TumblrPostSubmissionInputType!]!" --- Array of submissions defining when to post (minimum 1 submission) ::: :::field{name="publishingStatus" type="PublishingStatusType"} Publishing status. Options: `READY_TO_PUBLISH`, `DRAFT` (default: `READY_TO_PUBLISH`) ::: :::field --- name: postTags type: "[ID!]" --- Array of post tag IDs to organize your content. See [Post Tags](https://developers.postpone.app/scheduling-posts/post-tags). ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ### TumblrPostSubmissionInputType Each submission defines a specific posting schedule: ::field-group :::field{name="postAt" type="DateTime"} When to publish the post (must be in the future). Required unless you pass `schedulingMode: QUEUE_NEXT` on the post when scheduling a new one, or the post is a draft. See [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). ::: :::field{name="id" type="ID"} ID of existing submission when updating (only for updates) ::: :: ## NPF Content Structure Tumblr uses the [Neue Post Format (NPF)](https://www.tumblr.com/docs/npf){rel=""nofollow""} for structured content. Here are common content block types: ### Text Block ```json { "type": "text", "text": "Your text content here", "subtype": "heading1" // optional: heading1, heading2, quirky, quote, indented, chat, ordered-list-item, unordered-list-item } ``` ### Image Block ```json { "type": "image", "media": [ { "url": "https://example.com/image.jpg", "type": "image/jpeg", "width": 1080, "height": 1080 } ] } ``` ### Link Block ```json { "type": "link", "url": "https://example.com", "title": "Link Title", "description": "Link description", "author": "Author Name", "site_name": "Site Name" } ``` ## Examples ### Simple Text Post Schedule a basic text post to Tumblr. ```json { "input": { "username": "myblog", "content": [ { "type": "text", "text": "Just discovered this amazing new coffee shop downtown! ☕ The atmosphere is perfect for getting work done, and their latte art is incredible. Definitely my new favorite spot for morning meetings." } ], "layout": [], "tags": ["coffee", "downtown", "productivity", "latte art", "new favorite"], "communityLabel": "FOR_EVERYONE", "submissions": [ { "postAt": "2025-12-01T10:00:00Z" } ] } } ``` ### Scheduling at the Next Open Slot Instead of picking a time yourself, pass `schedulingMode: QUEUE_NEXT` and leave `postAt` out. Postpone puts the post in the next open slot on the account's [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""}, skipping any slot that already has a post on it. ```json { "input": { "username": "myblog", "schedulingMode": "QUEUE_NEXT", "content": [ { "type": "text", "text": "This one goes out at my next open slot." } ], "layout": [], "communityLabel": "FOR_EVERYONE", "submissions": [{}] } } ``` Leave `postAt` off the submissions — the resolved slot becomes the date for all of them. The account needs a schedule set up for this to work, `postAt` and `QUEUE_NEXT` can't be combined, and `updateScheduledTumblrPost` rejects `schedulingMode`. See [Scheduling at the Next Open Slot](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot) for the full rules. ### Image Post with Caption Schedule an image post with descriptive text. ```json { "input": { "username": "myblog", "content": [ { "type": "image", "media": [ { "url": "https://example.com/sunset-photo.jpg", "type": "image/jpeg", "width": 1080, "height": 1350 } ] }, { "type": "text", "text": "Golden hour magic from last night's walk along the beach. There's something so peaceful about watching the sun disappear into the horizon. 🌅✨" } ], "layout": [ { "type": "rows", "display": [ {"blocks": [0]}, {"blocks": [1]} ] } ], "tags": ["photography", "sunset", "beach", "golden hour", "peaceful", "nature"], "communityLabel": "FOR_EVERYONE", "submissions": [ { "postAt": "2025-12-01T18:30:00Z" } ] } } ``` ### Quote Post Schedule a quote post with attribution. ```json { "input": { "username": "myblog", "content": [ { "type": "text", "subtype": "quote", "text": "The future belongs to those who believe in the beauty of their dreams." }, { "type": "text", "text": "— Eleanor Roosevelt" } ], "layout": [ { "type": "rows", "display": [ {"blocks": [0]}, {"blocks": [1]} ] } ], "tags": ["quotes", "inspiration", "Eleanor Roosevelt", "dreams", "motivation"], "communityLabel": "NONE", "submissions": [ { "postAt": "2025-12-01T14:00:00Z" } ] } } ``` ### Link Post with Commentary Schedule a link post with your thoughts and commentary. ```json { "input": { "username": "myblog", "content": [ { "type": "text", "text": "Fascinating article about the future of sustainable technology! The innovations in solar panel efficiency are particularly exciting. We're definitely moving in the right direction for a greener future. 🌱" }, { "type": "link", "url": "https://example.com/sustainable-tech-article", "title": "Revolutionary Advances in Sustainable Technology", "description": "Exploring the latest breakthroughs in renewable energy and eco-friendly innovations that are shaping our future.", "author": "Dr. Jane Smith", "site_name": "Green Tech Today" } ], "layout": [ { "type": "rows", "display": [ {"blocks": [0]}, {"blocks": [1]} ] } ], "tags": ["sustainability", "technology", "renewable energy", "green tech", "innovation", "environment"], "sourceUrl": "https://example.com/sustainable-tech-article", "communityLabel": "NONE", "submissions": [ { "postAt": "2025-12-01T16:15:00Z" } ] } } ``` ### Mature Content Post Schedule a post with mature content labeling. ```json { "input": { "username": "myblog", "content": [ { "type": "text", "text": "Exploring themes of identity and self-expression through art. This piece represents the journey of finding yourself in a world that constantly tries to define you." }, { "type": "image", "media": [ { "url": "https://example.com/artistic-expression.jpg", "type": "image/jpeg", "width": 800, "height": 1200 } ] } ], "layout": [ { "type": "rows", "display": [ {"blocks": [0]}, {"blocks": [1]} ] } ], "tags": ["art", "identity", "self expression", "personal journey", "creative"], "communityLabel": "MATURE", "submissions": [ { "postAt": "2025-12-01T20:00:00Z" } ] } } ``` ## Response Types ### Success Response ```json { "data": { "scheduleTumblrPost": { "success": true, "errors": [], "post": { "id": "123", "socialAccount": { "id": "456", "username": "myblog" }, "publishingStatus": "READY_TO_PUBLISH", "content": [ { "type": "text", "text": "Just discovered this amazing new coffee shop downtown! ☕ The atmosphere is perfect for getting work done, and their latte art is incredible. Definitely my new favorite spot for morning meetings." } ], "layout": [], "tags": ["coffee", "downtown", "productivity", "latte art", "new favorite"], "communityLabel": "NONE", "sourceUrl": null, "gallery": null, "submissions": [ { "id": "789", "postAt": "2025-12-01T10:00:00Z" } ] } } } } ``` ### Error Response ```json { "data": { "scheduleTumblrPost": { "success": false, "errors": [ { "field": "content", "message": "Tumblr post content cannot be empty." } ], "post": null } } } ``` ## Updating a Post To update an existing scheduled post, use the same input type with the corresponding update mutation and include the post `id`: ```graphql [Mutation] mutation UpdateScheduledTumblrPost($input: ScheduleTumblrPostInput!) { updateScheduledTumblrPost(input: $input) { success errors { field message } post { id } } } ``` Pass the `id` of the post you want to update in the input, along with the fields you want to change: ```json { "input": { "id": "12345", "username": "myblog", "submissions": [{"postAt": "2025-12-01T15:30:00Z"}] } } ``` ::note The update mutation uses the same input type as the create mutation. Include the `id` field to specify which post to update. Only scheduled posts that have not been published yet can be updated. :: `postAt` is required on an update, so every update states the date the post goes out on — pass the date it already has to leave it where it is. A draft staying a draft is exempt, and a draft you're promoting to `READY_TO_PUBLISH` needs a date of your choosing; see [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). `schedulingMode` is not accepted on an update at all; see [Scheduling at the Next Open Slot](https://developers.postpone.app/#scheduling-at-the-next-open-slot). ## Validation Rules ::note The API enforces Tumblr's content policies and NPF requirements: :: - **Content Format**: Must use valid NPF (Neue Post Format) structure - **Tags**: Maximum 30 tags per post, each tag max 139 characters - **Text Length**: No hard limit, but very long posts may be truncated in feeds - **Image Requirements**: JPEG, PNG, GIF, WebP formats supported - **Image Size**: Maximum 10MB per image - **Video Support**: MP4 format, maximum 100MB - **Community Labels**: Must be correctly applied for mature content - **NPF Validation**: Content and layout must follow NPF specifications - **Scheduling**: Posts must be scheduled for future dates only - **Account Limits**: Respects your plan's monthly post limits ## Common Errors ::accordion :::accordion-item --- icon: i-lucide-alert-circle label: Tumblr account is not connected --- The specified username is not connected to your Postpone account. Connect the account in your settings first. ::: :::accordion-item{icon="i-lucide-code" label="Invalid NPF content structure"} Your content doesn't follow the NPF format correctly. Ensure all content blocks have the required fields and valid types. ::: :::accordion-item --- icon: i-lucide-file-text label: Tumblr post content cannot be empty --- You must provide content for your post. Include at least one content block in the NPF structure. ::: :::accordion-item{icon="i-lucide-tag" label="Too many tags"} You've exceeded the maximum of 30 tags per post. Remove some tags to comply with Tumblr's limits. ::: :::accordion-item{icon="i-lucide-type" label="Tag too long"} One or more tags exceed 139 characters. Shorten your tags to meet Tumblr's requirements. ::: :::accordion-item{icon="i-lucide-file-x" label="Invalid image format"} Your image format is not supported. Use JPEG, PNG, GIF, or WebP format for Tumblr posts. ::: :::accordion-item --- icon: i-lucide-hard-drive label: Image file size exceeds limit --- Your image file is too large. Tumblr supports images up to 10MB in size. ::: :::accordion-item{icon="i-lucide-layout" label="Invalid layout structure"} Your layout configuration doesn't follow NPF layout specifications. Check the layout format and block references. ::: :::accordion-item --- icon: i-lucide-shield-alert label: Community label required for mature content --- Your content may require mature labeling. Set `communityLabel` to `MATURE` for adult content. ::: :::accordion-item --- icon: i-lucide-calendar-x label: Post date cannot be in the past --- The `postAt` timestamp must be in the future. Check your timezone settings. ::: :::accordion-item --- icon: i-lucide-calendar-off label: Provide a post date, or use QUEUE_NEXT --- A new post has to say when it goes out: pass a `postAt`, or `schedulingMode: QUEUE_NEXT`. Drafts are exempt: leave `postAt` off one and it is parked on a placeholder date about 30 days out. ::: :::accordion-item{icon="i-lucide-calendar-off" label="Provide a post date"} `schedulingMode` isn't accepted on an update, so a `postAt` is the only way to say when the post goes out. You'll also see it when promoting a parked draft to `publishingStatus: READY_TO_PUBLISH` without giving it a date. ::: :::accordion-item --- icon: i-lucide-calendar-clock label: Account has no schedule set up --- `schedulingMode: QUEUE_NEXT` needs an [Account Schedule](https://help.postpone.app/scheduling/publishing/account-schedules){rel=""nofollow""} on the account you're posting to. Set one up, or pass an explicit `postAt`. ::: :::accordion-item --- icon: i-lucide-pencil-off label: schedulingMode is not supported when updating a post --- `schedulingMode` is only accepted when scheduling a new post. To move a post that is already scheduled, pass a `postAt` to `updateScheduledTumblrPost`. ::: :::accordion-item --- icon: i-lucide-trending-up label: Over plan limits for scheduled month --- You've reached your plan's Tumblr post limit for the scheduled month. Upgrade your plan or schedule for a different month. ::: :: # Telegram ## Mutation Use the `scheduleTelegramPost` mutation to schedule posts to Telegram channels and groups: ```graphql [Mutation] mutation ScheduleTelegramPost($input: ScheduleTelegramPostInput!) { scheduleTelegramPost(input: $input) { success errors { field message } post { id socialAccount { id username } publishingStatus text disableLinkPreview linkPreviewSize disableNotification pinAfterPublishing protectContent mediaSpoiler captionAboveMedia paidStarCount buttons { text url } gallery { id galleryMediaSet { id media { url mimeType } } } submissions { id postAt } } } } ``` ## Input Parameters ### ScheduleTelegramPostInput ::field-group :::field{required name="username" type="String!"} The username of the connected Telegram channel or group to post to, without the `@`. Private chats have no `@username`, so their numeric chat ID (e.g. `-1001234567890`) is used as the username instead. ::: :::field{name="schedulingMode" type="SchedulingModeType"} How the post date is decided. Options: `SPECIFIC_TIME`, `QUEUE_NEXT` (default: `SPECIFIC_TIME`). Only accepted when scheduling a new post. See [Scheduling at the Next Open Slot](https://developers.postpone.app/scheduling-posts#scheduling-at-the-next-open-slot). ::: :::field{name="text" type="String"} The message text (max 4,096 characters), or the caption when media is attached (max 1,024 characters). Plain text is published as typed and links are clickable automatically. To format text, use HTML with the tags Telegram supports (see [Formatting](https://developers.postpone.app/#formatting)). Limits count the visible characters. Required unless media is attached. ::: :::field --- name: media type: "[SimpleMediaInput]" --- Media files to attach to the post: up to 10 photos and videos (sent as an album), or a single GIF. Each item references a file by `id`, `name`, or `url`. See [Attaching Media](https://developers.postpone.app/scheduling-posts#attaching-media). Only supported when scheduling new posts, not when updating existing ones. ::: :::field{name="disableLinkPreview" type="Boolean"} Do not show a preview for the links in the message (default: `false`) ::: :::field{name="linkPreviewSize" type="TelegramLinkPreviewSize"} How the preview of the first link shows its image. Options: `DEFAULT`, `LARGE`, `SMALL` (default: `DEFAULT`). Applies to text posts only, since media captions never show link previews. ::: :::field{name="mediaSpoiler" type="Boolean"} Blur the attached photos and videos until they are tapped (default: `false`) ::: :::field{name="captionAboveMedia" type="Boolean"} Show the text above the attached photos and videos instead of below them (default: `false`) ::: :::field{name="paidStarCount" type="Int"} Publish the media as paid media: subscribers see it blurred until they pay this many Telegram Stars (1 to 25,000). In a channel the Stars are credited to the channel's balance; in a group, Telegram credits them to the bot's balance. Photos and videos only, no GIFs. `0` or omitted publishes the media normally (default: `0`). See [Paid media](https://developers.postpone.app/#paid-media). ::: :::field --- name: buttons type: "[TelegramButtonInput!]" --- Up to 10 link buttons shown under the message, one per row. Each button has a `text` label (max 64 characters) and an `https://`, `http://` or `tg://` `url`. Not available on albums, unless the album is paid media. ::: :::field{name="disableNotification" type="Boolean"} Send the message silently, without a notification sound (default: `false`) ::: :::field{name="pinAfterPublishing" type="Boolean"} Pin the message in the chat right after it is published. In a channel, the bot needs the "Manage Messages" permission; in a group, it must be an administrator with the "Pin Messages" permission (default: `false`) ::: :::field{name="protectContent" type="Boolean"} Prevent the message from being forwarded or saved (default: `false`) ::: :::field --- required: true name: submissions type: "[TelegramPostSubmissionInputType!]!" --- Array of submissions defining when to post (minimum 1 submission) ::: :::field{name="publishingStatus" type="PublishingStatusType"} Publishing status. Options: `READY_TO_PUBLISH`, `DRAFT` (default: `READY_TO_PUBLISH`) ::: :::field --- name: postTags type: "[ID!]" --- Array of post tag IDs to organize your content. See [Post Tags](https://developers.postpone.app/scheduling-posts/post-tags). ::: :::field{name="id" type="ID"} ID of existing post when updating (only for updates) ::: :: ### TelegramPostSubmissionInputType Each submission defines a specific posting schedule: ::field-group :::field{name="postAt" type="DateTime"} When to publish the post (must be in the future). Required unless you pass `schedulingMode: QUEUE_NEXT` on the post when scheduling a new one, or the post is a draft. See [Drafts without a date](https://developers.postpone.app/scheduling-posts#drafts-without-a-date). ::: :::field{name="id" type="ID"} ID of existing submission when updating (only for updates) ::: :: ## Examples ### Simple Telegram Post Schedule a text post with a link. Telegram makes the link clickable and shows a preview for it. ```json { "input": { "username": "mychannel", "text": "Our September update is live! Read what's new: https://example.com/september-update", "submissions": [ { "postAt": "2026-10-01T09:00:00Z" } ] } } ``` ### Silent Post without a Link Preview ```json { "input": { "username": "mychannel", "text": "Reminder: the community call starts in one hour. Join here: https://example.com/call", "disableLinkPreview": true, "disableNotification": true, "submissions": [ { "postAt": "2026-10-01T16:00:00Z" } ] } } ``` ### Telegram Album with a Caption Format the text with Telegram's HTML tags and add link buttons under the post. Paragraphs (`

`) become line breaks, and the character limit counts the text readers see. ```json { "input": { "username": "mychannel", "text": "

Big news: our September update is live.

Read the full changelog, or reveal the surprise feature.

", "linkPreviewSize": "LARGE", "buttons": [ {"text": "Read the update", "url": "https://example.com/september-update"}, {"text": "Get support", "url": "https://example.com/support"} ], "submissions": [{"postAt": "2026-10-01T09:00:00Z"}] } } ``` Charge Telegram Stars for the media. Subscribers see the photos and videos blurred until they pay, and the caption and buttons stay visible. ```json { "input": { "username": "mychannel", "text": "Behind the scenes of this week's shoot 🎬", "media": [{"name": "shoot-1.jpg"}, {"name": "shoot-2.mp4"}], "paidStarCount": 50, "buttons": [{"text": "More exclusives", "url": "https://example.com/exclusives"}], "submissions": [{"postAt": "2026-10-01T09:00:00Z"}] } } ``` Attach up to 10 photos and videos. The text is used as the album caption (max 1,024 characters). ```json { "input": { "username": "mychannel", "text": "Highlights from this week's photo walk 📸", "media": [ { "url": "https://example.com/photo-1.jpg" }, { "url": "https://example.com/photo-2.jpg" }, { "name": "photo-walk-video.mp4" } ], "submissions": [ { "postAt": "2026-10-02T12:00:00Z" } ] } } ``` ### Pinned Announcement Pin the message after it is published. In a channel, the bot needs the "Manage Messages" permission; in a group, it must be an administrator with the "Pin Messages" permission. ```json { "input": { "username": "-1001234567890", "text": "📌 Rules of the group: be kind, stay on topic, and have fun.", "pinAfterPublishing": true, "protectContent": true, "submissions": [ { "postAt": "2026-10-03T08:00:00Z" } ] } } ``` ## Response Types ### Success Response ```json { "data": { "scheduleTelegramPost": { "success": true, "errors": [], "post": { "id": "123", "socialAccount": { "id": "456", "username": "mychannel" }, "publishingStatus": "READY_TO_PUBLISH", "text": "Our September update is live! Read what's new: https://example.com/september-update", "disableLinkPreview": false, "linkPreviewSize": "DEFAULT", "disableNotification": false, "pinAfterPublishing": false, "protectContent": false, "mediaSpoiler": false, "captionAboveMedia": false, "paidStarCount": 0, "buttons": [], "gallery": null, "submissions": [ { "id": "789", "postAt": "2026-10-01T09:00:00Z" } ] } } } } ``` ### Error Response ```json { "data": { "scheduleTelegramPost": { "success": false, "errors": [ { "field": "text", "message": "Telegram limits captions to 1,024 characters when media is attached." } ], "post": null } } } ``` ## Updating a Post To update an existing scheduled post, use the same input type with the `updateScheduledTelegramPost` mutation and include the post `id`: ```graphql [Mutation] mutation UpdateScheduledTelegramPost($input: ScheduleTelegramPostInput!) { updateScheduledTelegramPost(input: $input) { success errors { field message } post { id } } } ``` ```json { "input": { "id": "12345", "username": "mychannel", "text": "Updated Telegram post!", "submissions": [{"id": "789", "postAt": "2026-10-01T15:30:00Z"}] } } ``` ::note The update mutation uses the same input type as the create mutation. Include the `id` field to specify which post to update. Only scheduled posts that have not been published yet can be updated. :: ## Formatting Plain text is published exactly as typed. To format a post, send `text` as HTML using only the tags Telegram supports: | Formatting | Tags | | -------------------------------------- | --------------------------------------------------------------------------- | | Bold, italic, underline, strikethrough | ``, ``, ``, `` (``, ``, ``, `` also work) | | Spoiler (hidden until tapped) | `` or `` | | Link | `` | | Code | `` inline, `
` for blocks                                         |
| Quote                                  | `
` | | Line breaks | `

` paragraphs or `
` | Escape `<`, `>` and `&` in formatted text as `<`, `>` and `&`. Other tags are removed and their text is kept. The 4,096 and 1,024 character limits count the visible text only. ## Paid media Set `paidStarCount` to publish the attached photos and videos as paid media. Telegram shows them blurred with the price, and each subscriber who pays that many Stars unlocks them permanently. The Stars are credited to the channel's balance, where the channel owner manages them in Telegram. When paid media is posted to a group, Telegram credits the Stars to the bot's balance instead. Paid media is one message however many items it holds, so it can carry a caption of up to 1,024 characters and link buttons. GIFs can't be paid media, and Telegram does not report channel purchases to bots, so Postpone can't show earnings. ## Validation Rules - **Text Length**: Maximum 4,096 characters, or 1,024 characters when media is attached, counting the visible text in UTF-16 code units as Telegram does (most emoji count as two) - **Buttons**: Up to 10 buttons with a label of at most 64 characters and an `https://`, `http://` or `tg://` URL; not allowed on albums, unless the album is paid media - **Paid media**: `paidStarCount` between 1 and 25,000 requires at least one photo or video and no GIFs - **Media**: Up to 10 photos (max 10MB each) and videos (max 50MB each) as an album, or a single GIF (max 50MB). Any image or video format Postpone accepts can be scheduled: images are converted to JPEG and videos to MP4 when the post is published - **Images**: Width plus height must not exceed 10,000 pixels, with a width to height ratio of at most 20 - **Pinning**: `pinAfterPublishing` requires the bot to be allowed to pin in the chat ("Manage Messages" in a channel, "Pin Messages" as a group administrator) - **Scheduling**: Posts must be scheduled for future dates only - **Account Limits**: Respects your plan's monthly post limits ## Common Errors ::accordion :::accordion-item --- icon: i-lucide-type label: Telegram limits captions to 1,024 characters when media is attached --- Shorten the text, or remove the media to use the 4,096 character limit of text-only posts. ::: :::accordion-item --- icon: i-lucide-mouse-pointer-click label: Telegram can't show buttons under posts with more than one media item --- Remove the buttons, or attach a single photo or video. Telegram albums cannot carry an inline keyboard. ::: :::accordion-item --- icon: i-lucide-star label: Paid media can only contain photos and videos, not GIFs --- Remove the GIF, or set `paidStarCount` to `0` to publish it as a normal post. ::: :::accordion-item --- icon: i-lucide-image label: GIFs must be posted on their own, not as part of an album --- Telegram albums can only hold photos and videos. Schedule the GIF as its own post. ::: :::accordion-item --- icon: i-lucide-pin-off label: The bot is not allowed to pin messages in this chat --- Turn on "Manage Messages" for the bot in the channel (or make it a group administrator with "Pin Messages") and reconnect the channel or group in Postpone, or turn off `pinAfterPublishing`. ::: :::accordion-item --- icon: i-lucide-calendar-x label: Post date cannot be in the past --- The `postAt` timestamp must be in the future. Check your timezone settings. ::: :::accordion-item --- icon: i-lucide-trending-up label: Over plan limits for scheduled month --- You've reached your plan's post limit for the scheduled month. Upgrade your plan or schedule for a different month. ::: :: # Post Time Series Metrics The `postTimeSeriesMetrics` query returns analytics metrics for posts over time, allowing you to track performance trends and patterns across different time periods. ## Query ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Query"} ```graphql query postTimeSeriesMetrics( $socialAccountIds: [ID] $startDate: DateTime! $endDate: DateTime! $metrics: [String] $groupBy: String $subreddits: [String] ) { postTimeSeriesMetrics( socialAccountIds: $socialAccountIds startDate: $startDate endDate: $endDate metrics: $metrics groupBy: $groupBy subreddits: $subreddits ) { date isoWeek isoYear metrics { metric value } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "socialAccountIds": ["123", "456"], "startDate": "2025-01-01T00:00:00Z", "endDate": "2025-01-31T23:59:59Z", "metrics": ["likes", "comments", "impressions", "posts"], "groupBy": "week" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "postTimeSeriesMetrics": [ { "date": "2025-01-01T00:00:00Z", "isoWeek": 1, "isoYear": 2024, "metrics": [ { "metric": "likes", "value": 1250 }, { "metric": "comments", "value": 84 }, { "metric": "impressions", "value": 15300 }, { "metric": "posts", "value": 12 } ] }, { "date": "2025-01-08T00:00:00Z", "isoWeek": 2, "isoYear": 2024, "metrics": [ { "metric": "likes", "value": 1890 }, { "metric": "comments", "value": 102 }, { "metric": "impressions", "value": 18650 }, { "metric": "posts", "value": 15 } ] } ] } } ``` :::: ::: :: ## Parameters ::field-group :::field --- name: socialAccountIds type: "[ID]" --- An array of social account IDs to retrieve metrics for. All accounts must be from the same platform. See [how to retrieve your social accounts](https://developers.postpone.app/queries/social-accounts). ::: :::field{required name="startDate" type="DateTime"} The start date for the analytics period. Must be in ISO 8601 format. ::: :::field{required name="endDate" type="DateTime"} The end date for the analytics period. Must be in ISO 8601 format. ::: :::field --- name: metrics type: "[String]" --- An array of metric names to retrieve. Available metrics vary by platform. See **Available Metrics** below. ::: :::field{name="groupBy" type="String"} How to group the time series data. Options: `"day"`, `"week"`, `"month"`. Default: `"week"`. ::: :::field --- name: subreddits type: "[String]" --- Filter metrics by specific subreddits (Reddit only). Pass subreddit names without the "r/" prefix. ::: :: ## Available Metrics The available metrics depend on the platform of your social accounts. See the [Platform Metrics](https://developers.postpone.app/analytics/platform-metrics) documentation for a complete list of metrics available for each platform. ::note All social accounts in a single query must be from the same platform. To retrieve metrics for multiple platforms, make separate queries. :: ## Examples ### Track Weekly Instagram Performance ```json { "socialAccountIds": ["789"], "startDate": "2025-01-01T00:00:00Z", "endDate": "2025-03-31T23:59:59Z", "metrics": ["likes", "comments", "impressions", "reach"], "groupBy": "week" } ``` ### Daily TikTok Metrics for Multiple Accounts ```json { "socialAccountIds": ["101", "102", "103"], "startDate": "2025-10-01T00:00:00Z", "endDate": "2025-10-31T23:59:59Z", "metrics": ["likes", "views", "shares"], "groupBy": "day" } ``` ### Reddit Performance by Subreddit ```json { "socialAccountIds": ["456"], "subreddits": ["gaming", "pcmasterrace", "buildapc"], "startDate": "2025-01-01T00:00:00Z", "endDate": "2025-01-31T23:59:59Z", "metrics": ["upvotes", "comments", "posts"], "groupBy": "week" } ``` # Aggregate Post Metrics The `aggregatePostMetrics` query returns aggregated analytics data for posts, providing both sum totals and average values for each metric across the specified time period. ## Query ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Query"} ```graphql query aggregatePostMetrics( $socialAccountIds: [ID] $startDate: DateTime! $endDate: DateTime! $subreddits: [String] $metrics: [String] ) { aggregatePostMetrics( socialAccountIds: $socialAccountIds startDate: $startDate endDate: $endDate subreddits: $subreddits metrics: $metrics ) { metrics { metric value aggregation } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "socialAccountIds": ["123"], "startDate": "2025-01-01T00:00:00Z", "endDate": "2025-01-31T23:59:59Z", "metrics": ["likes", "comments", "impressions"] } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "aggregatePostMetrics": { "metrics": [ { "metric": "posts", "value": 45, "aggregation": "sum" }, { "metric": "posts", "value": 1.5, "aggregation": "avg" }, { "metric": "likes", "value": 8420, "aggregation": "sum" }, { "metric": "likes", "value": 187.1, "aggregation": "avg" }, { "metric": "comments", "value": 612, "aggregation": "sum" }, { "metric": "comments", "value": 13.6, "aggregation": "avg" }, { "metric": "impressions", "value": 125300, "aggregation": "sum" }, { "metric": "impressions", "value": 2784.4, "aggregation": "avg" } ] } } } ``` :::: ::: :: ## Parameters ::field-group :::field --- name: socialAccountIds type: "[ID]" --- An array of social account IDs to retrieve metrics for. All accounts must be from the same platform. See [how to retrieve your social accounts](https://developers.postpone.app/queries/social-accounts). ::: :::field{required name="startDate" type="DateTime"} The start date for the analytics period. Must be in ISO 8601 format. ::: :::field{required name="endDate" type="DateTime"} The end date for the analytics period. Must be in ISO 8601 format. ::: :::field --- name: subreddits type: "[String]" --- Filter metrics by specific subreddits (Reddit only). Pass subreddit names without the "r/" prefix. ::: :::field --- name: metrics type: "[String]" --- An array of metric names to retrieve. The `posts` metric is always included automatically. See **Available Metrics** for platform-specific options. ::: :: ## Response Structure The response includes both **sum** and **avg** aggregations for each metric: - **sum**: The total count across all posts in the time period - **avg**: The average value per post The `posts` metric is always included automatically and represents the total number of posts published during the period. ## Available Metrics The available metrics are platform-specific. See the [Platform Metrics](https://developers.postpone.app/analytics/platform-metrics) documentation for a complete list of metrics available for each platform. ::note All social accounts in a single query must be from the same platform. To retrieve metrics for multiple platforms, make separate queries. :: ## Examples ### Instagram Account Summary Get aggregated metrics for an Instagram account to display in a dashboard: ```json { "socialAccountIds": ["789"], "startDate": "2025-01-01T00:00:00Z", "endDate": "2025-03-31T23:59:59Z", "metrics": ["likes", "comments", "impressions", "reach", "shares"] } ``` ### TikTok Monthly Report Generate a monthly report for multiple TikTok accounts: ```json { "socialAccountIds": ["101", "102", "103"], "startDate": "2025-10-01T00:00:00Z", "endDate": "2025-10-31T23:59:59Z", "metrics": ["likes", "comments", "shares", "impressions"] } ``` ### Twitter Engagement Analysis Calculate average engagement metrics for a Twitter account: ```json { "socialAccountIds": ["555"], "startDate": "2025-06-01T00:00:00Z", "endDate": "2025-06-30T23:59:59Z", "metrics": ["likes", "replies", "retweets", "impressions", "quotes"] } ``` ## Use Cases This query is ideal for: - **Dashboard Widgets**: Display high-level KPIs and summary statistics - **Performance Reports**: Generate monthly or quarterly performance reports - **Comparison Analysis**: Compare performance across different time periods - **Goal Tracking**: Monitor progress toward engagement targets - **ROI Calculations**: Calculate average returns per post for campaign analysis # Aggregate Social Account Post Metrics The `aggregateSocialAccountPostMetrics` query returns aggregated analytics data for posts, grouped by social account. This allows you to compare performance across multiple accounts or platforms. ## Query ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Query"} ```graphql query aggregateSocialAccountPostMetrics( $socialAccountIds: [ID]! $startDate: DateTime! $endDate: DateTime! $metrics: [String]! ) { aggregateSocialAccountPostMetrics( socialAccountIds: $socialAccountIds startDate: $startDate endDate: $endDate metrics: $metrics ) { socialAccount { id username formattedUsername avatarUrl platform } metrics { metric value aggregation } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "socialAccountIds": ["123", "456", "789"], "startDate": "2025-01-01T00:00:00Z", "endDate": "2025-01-31T23:59:59Z", "metrics": ["likes", "comments", "followers"] } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "aggregateSocialAccountPostMetrics": [ { "socialAccount": { "id": "123", "username": "myinstagram", "formattedUsername": "@myinstagram", "avatarUrl": "https://example.com/avatar1.jpg", "platform": "instagram" }, "metrics": [ { "metric": "likes", "value": 12450, "aggregation": "sum" }, { "metric": "likes", "value": 276.7, "aggregation": "avg" }, { "metric": "comments", "value": 892, "aggregation": "sum" }, { "metric": "comments", "value": 19.8, "aggregation": "avg" }, { "metric": "followers", "value": 15300, "aggregation": "sum" } ] }, { "socialAccount": { "id": "456", "username": "mytiktok", "formattedUsername": "@mytiktok", "avatarUrl": "https://example.com/avatar2.jpg", "platform": "tiktok" }, "metrics": [ { "metric": "likes", "value": 45200, "aggregation": "sum" }, { "metric": "likes", "value": 1506.7, "aggregation": "avg" }, { "metric": "comments", "value": 3104, "aggregation": "sum" }, { "metric": "comments", "value": 103.5, "aggregation": "avg" }, { "metric": "followers", "value": 28500, "aggregation": "sum" } ] }, { "socialAccount": { "id": "789", "username": "mytwitter", "formattedUsername": "@mytwitter", "avatarUrl": "https://example.com/avatar3.jpg", "platform": "twitter" }, "metrics": [ { "metric": "likes", "value": 8920, "aggregation": "sum" }, { "metric": "likes", "value": 148.7, "aggregation": "avg" }, { "metric": "comments", "value": 412, "aggregation": "sum" }, { "metric": "comments", "value": 6.9, "aggregation": "avg" }, { "metric": "followers", "value": 8450, "aggregation": "sum" } ] } ] } } ``` :::: ::: :: ## Parameters ::field-group :::field --- required: true name: socialAccountIds type: "[ID]" --- An array of social account IDs to retrieve metrics for. Unlike other analytics queries, this query **supports multiple platforms** in a single request. See [how to retrieve your social accounts](https://developers.postpone.app/queries/social-accounts). ::: :::field{required name="startDate" type="DateTime"} The start date for the analytics period. Must be in ISO 8601 format. ::: :::field{required name="endDate" type="DateTime"} The end date for the analytics period. Must be in ISO 8601 format. ::: :::field --- required: true name: metrics type: "[String]" --- An array of metric names to retrieve. Use platform-agnostic metric names (e.g., `"likes"` instead of `"upvotes"`). ::: :: ## Response Structure The response includes an array of results, one per social account, each containing: - **socialAccount**: Details about the social account including username, platform, and avatar - **metrics**: An array of metric values with both **sum** and **avg** aggregations For each requested metric, you'll receive: - **sum**: The total count across all posts in the time period - **avg**: The average value per post ### Special Metric: Followers The `followers` metric is special and returns the most recent follower count for the account during the specified time period (not a sum or average of follower changes). ## Platform-Agnostic Metrics This query uses platform-agnostic metric names that map to platform-specific equivalents: - **likes**: Maps to likes (most platforms), upvotes (Reddit), notes (Tumblr), or reactions (Telegram) - **comments**: Maps to comments (most platforms), replies (Twitter, Threads, Bluesky, Telegram) - **followers**: Returns the current follower count - **impressions**: Available for platforms that track impressions - **views**: Available for platforms that track views - **shares**: Available for platforms that track shares - **reach**: Available for platforms that track reach ::note Unlike `postTimeSeriesMetrics` and `aggregatePostMetrics`, this query **supports querying multiple platforms at once**. This makes it ideal for cross-platform comparison dashboards. :: ## Examples ### Compare Instagram and TikTok Performance ```json { "socialAccountIds": ["instagram-123", "tiktok-456"], "startDate": "2025-01-01T00:00:00Z", "endDate": "2025-03-31T23:59:59Z", "metrics": ["likes", "comments", "shares", "followers"] } ``` ### Multi-Platform Dashboard Get metrics for all connected accounts across all platforms: ```json { "socialAccountIds": ["123", "456", "789", "101", "102"], "startDate": "2025-10-01T00:00:00Z", "endDate": "2025-10-31T23:59:59Z", "metrics": ["likes", "comments", "followers"] } ``` ### Client Reporting Generate a report showing performance across all accounts managed for a specific client: ```json { "socialAccountIds": ["client-ig-1", "client-tw-1", "client-tk-1"], "startDate": "2025-06-01T00:00:00Z", "endDate": "2025-06-30T23:59:59Z", "metrics": ["likes", "comments", "impressions", "followers"] } ``` ### Follower Growth Tracking Track follower counts across all platforms: ```json { "socialAccountIds": ["123", "456", "789", "101"], "startDate": "2025-01-01T00:00:00Z", "endDate": "2025-12-31T23:59:59Z", "metrics": ["followers"] } ``` ## Use Cases This query is ideal for: - **Cross-Platform Dashboards**: Display unified metrics across all your social accounts - **Account Comparison**: Compare performance between different accounts - **Client Reporting**: Generate reports for agency clients with multiple accounts - **Platform Analysis**: Identify which platforms perform best for your content - **Portfolio Overview**: Get a bird's-eye view of all managed accounts # Post Tag Metrics The `postTagMetrics` query returns analytics data aggregated by post tags, allowing you to analyze performance based on how you've categorized your content. ## Query ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Query"} ```graphql query postTagMetrics( $startDate: DateTime! $endDate: DateTime! $supportsEngagementRate: Boolean ) { postTagMetrics( startDate: $startDate endDate: $endDate supportsEngagementRate: $supportsEngagementRate ) { postTag { id name color } metrics { metric value aggregation } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "startDate": "2025-01-01T00:00:00Z", "endDate": "2025-03-31T23:59:59Z", "supportsEngagementRate": true } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "postTagMetrics": [ { "postTag": { "id": "tag-001", "name": "Product Launch", "color": "#FF5733" }, "metrics": [ { "metric": "posts", "value": 28, "aggregation": "sum" }, { "metric": "likes", "value": 15420, "aggregation": "sum" }, { "metric": "likes", "value": 550.7, "aggregation": "avg" }, { "metric": "comments", "value": 892, "aggregation": "sum" }, { "metric": "comments", "value": 31.9, "aggregation": "avg" }, { "metric": "engagement_rate", "value": 4.8, "aggregation": "avg" } ] }, { "postTag": { "id": "tag-002", "name": "Behind the Scenes", "color": "#3498DB" }, "metrics": [ { "metric": "posts", "value": 42, "aggregation": "sum" }, { "metric": "likes", "value": 8920, "aggregation": "sum" }, { "metric": "likes", "value": 212.4, "aggregation": "avg" }, { "metric": "comments", "value": 412, "aggregation": "sum" }, { "metric": "comments", "value": 9.8, "aggregation": "avg" }, { "metric": "engagement_rate", "value": 3.2, "aggregation": "avg" } ] }, { "postTag": { "id": "tag-003", "name": "Educational", "color": "#2ECC71" }, "metrics": [ { "metric": "posts", "value": 35, "aggregation": "sum" }, { "metric": "likes", "value": 12100, "aggregation": "sum" }, { "metric": "likes", "value": 345.7, "aggregation": "avg" }, { "metric": "comments", "value": 1205, "aggregation": "sum" }, { "metric": "comments", "value": 34.4, "aggregation": "avg" }, { "metric": "engagement_rate", "value": 5.4, "aggregation": "avg" } ] } ] } } ``` :::: ::: :: ## Parameters ::field-group :::field{required name="startDate" type="DateTime"} The start date for the analytics period. Must be in ISO 8601 format. ::: :::field{required name="endDate" type="DateTime"} The end date for the analytics period. Must be in ISO 8601 format. ::: :::field{name="supportsEngagementRate" type="Boolean"} Whether to include engagement rate calculations in the metrics. Default: `false`. When enabled, the query calculates engagement rate where applicable. ::: :: ## Response Structure The response includes an array of results, one per post tag used during the time period. Each result contains: - **postTag**: Details about the tag including ID, name, and color - **metrics**: An array of metric values aggregated across all posts with this tag For each metric, you'll receive: - **sum**: The total count across all posts with this tag - **avg**: The average value per post with this tag The `posts` metric shows how many posts were tagged with this tag during the period. ## Metrics Returned The query automatically includes relevant metrics based on the platforms where tagged posts were published: ### Core Metrics (All Platforms) - **posts**: Number of posts with this tag - **likes**: Total and average likes (or platform equivalent) - **comments**: Total and average comments (or platform equivalent) ### Platform-Specific Metrics Depending on which platforms your tagged posts were published to, you may also see: - **impressions**: Total impressions (Instagram, Twitter, Facebook, LinkedIn, TikTok) - **views**: Total views (TikTok, YouTube, Threads) - **shares**: Total shares (Instagram, TikTok, Facebook) - **reach**: Total reach (Instagram, Facebook) - **engagement_rate**: Average engagement rate (when `supportsEngagementRate` is enabled) ::note The metrics returned are automatically determined by the platforms where your tagged posts were published. This query aggregates data across all platforms. :: ## Understanding Engagement Rate When `supportsEngagementRate` is set to `true`, the query includes an engagement rate calculation. The engagement rate is calculated as: ```text (likes + comments + shares) / impressions × 100 ``` This metric is only calculated for platforms that provide impression data. ## Examples ### Compare Content Categories Analyze which types of content perform best: ```json { "startDate": "2025-01-01T00:00:00Z", "endDate": "2025-03-31T23:59:59Z", "supportsEngagementRate": true } ``` ### Quarterly Campaign Analysis Review performance of different campaign tags over a quarter: ```json { "startDate": "2025-07-01T00:00:00Z", "endDate": "2025-09-30T23:59:59Z", "supportsEngagementRate": false } ``` ### Monthly Content Review Get monthly metrics to inform your content strategy: ```json { "startDate": "2025-10-01T00:00:00Z", "endDate": "2025-10-31T23:59:59Z", "supportsEngagementRate": true } ``` ### Year-End Report Generate an annual report showing which content tags performed best: ```json { "startDate": "2025-01-01T00:00:00Z", "endDate": "2025-12-31T23:59:59Z", "supportsEngagementRate": true } ``` ## Use Cases This query is ideal for: - **Content Strategy**: Identify which content categories resonate most with your audience - **Campaign Analysis**: Compare performance across different campaign types - **Topic Performance**: Discover which topics generate the most engagement - **Content Planning**: Use historical tag performance to inform future content plans - **ROI Tracking**: Measure the effectiveness of different content themes - **Team Reporting**: Show content team which themes are performing best ## Working with Post Tags Post tags are created and managed in your Postpone account. To use this query effectively: 1. **Tag your posts consistently**: Apply tags to posts as you create them 2. **Use meaningful names**: Choose tag names that reflect content categories or campaigns 3. **Organize by theme**: Group related content under the same tags 4. **Review regularly**: Use this query periodically to inform your content strategy ::tip Post tags are account-wide, meaning this query automatically includes data from all your social accounts across all platforms. This gives you a holistic view of how different content types perform regardless of where they're posted. :: # Multi-Platform Analytics Submissions The `multiPlatformAnalyticsSubmissions` query returns a paginated list of individual post submissions with their associated metrics. This query supports searching, filtering, and sorting across all platforms simultaneously. ## Query ::code-collapse :::tabs ::::tabs-item{icon="i-lucide-code" label="Query"} ```graphql query multiPlatformAnalyticsSubmissions( $socialAccountIds: [ID] $clientId: ID $startDate: DateTime! $endDate: DateTime! $query: String $page: Int $limit: Int $orderBy: SubmissionAnalyticsOrderingType ) { multiPlatformAnalyticsSubmissions( socialAccountIds: $socialAccountIds clientId: $clientId startDate: $startDate endDate: $endDate query: $query page: $page limit: $limit orderBy: $orderBy ) { total objects { platformId dateSubmitted socialAccount { id username formattedUsername avatarUrl platform } text thumbnailUrl url target metrics { metric value } postTags { id name color } } } } ``` :::: ::::tabs-item{icon="i-lucide-settings" label="Variables"} ```json { "socialAccountIds": ["123", "456", "789"], "startDate": "2025-01-01T00:00:00Z", "endDate": "2025-01-31T23:59:59Z", "query": "product launch", "page": 1, "limit": 20, "orderBy": "LIKES_DESC" } ``` :::: ::::tabs-item{icon="i-lucide-eye" label="Result"} ```json { "data": { "multiPlatformAnalyticsSubmissions": { "total": 47, "objects": [ { "platformId": "1234567890", "dateSubmitted": "2025-01-15T14:30:00Z", "socialAccount": { "id": "123", "username": "myinstagram", "formattedUsername": "@myinstagram", "avatarUrl": "https://example.com/avatar1.jpg", "platform": "instagram" }, "text": "Excited to announce our new product launch! 🚀", "thumbnailUrl": "https://example.com/thumb.jpg", "url": "https://instagram.com/p/abc123", "target": null, "metrics": [ { "metric": "likes", "value": 2450 }, { "metric": "comments", "value": 184 }, { "metric": "impressions", "value": 28500 }, { "metric": "reach", "value": 24100 }, { "metric": "shares", "value": 92 } ], "postTags": [ { "id": "tag-001", "name": "Product Launch", "color": "#FF5733" } ] }, { "platformId": "tweet-id-xyz", "dateSubmitted": "2025-01-12T09:15:00Z", "socialAccount": { "id": "456", "username": "mytwitter", "formattedUsername": "@mytwitter", "avatarUrl": "https://example.com/avatar2.jpg", "platform": "twitter" }, "text": "Big announcement coming soon! Our product launch is just around the corner. Stay tuned! 📱", "thumbnailUrl": null, "url": "https://twitter.com/mytwitter/status/xyz", "target": null, "metrics": [ { "metric": "likes", "value": 1820 }, { "metric": "replies", "value": 67 }, { "metric": "retweets", "value": 342 }, { "metric": "impressions", "value": 45200 }, { "metric": "quotes", "value": 28 } ], "postTags": [ { "id": "tag-001", "name": "Product Launch", "color": "#FF5733" }, { "id": "tag-005", "name": "Teaser", "color": "#9B59B6" } ] } ] } } } ``` :::: ::: :: ## Parameters ::field-group :::field --- name: socialAccountIds type: "[ID]" --- Filter submissions to specific social accounts. If omitted, includes all accounts you have access to. See [how to retrieve your social accounts](https://developers.postpone.app/queries/social-accounts). ::: :::field{name="clientId" type="ID"} Filter submissions to social accounts belonging to a specific client (for agency users). ::: :::field{required name="startDate" type="DateTime"} The start date for submissions. Only posts submitted after this date will be included. Must be in ISO 8601 format. ::: :::field{required name="endDate" type="DateTime"} The end date for submissions. Only posts submitted before this date will be included. Must be in ISO 8601 format. ::: :::field{name="query" type="String"} Search term to filter submissions by text content. Searches the post text/caption for matches (case-insensitive). ::: :::field{name="page" type="Int"} The page number for pagination. Default: `1`. ::: :::field{name="limit" type="Int"} The number of submissions per page. Default: `100`. Maximum: `100`. ::: :::field{name="orderBy" type="SubmissionAnalyticsOrderingType"} How to sort the results. Default: `DATE_SUBMITTED_DESC`. See **Sorting Options** below. ::: :: ## Sorting Options The `orderBy` parameter accepts the following values: - `DATE_SUBMITTED_ASC` - Oldest first - `DATE_SUBMITTED_DESC` - Newest first (default) - `LIKES_ASC` - Least likes first - `LIKES_DESC` - Most likes first - `REPLIES_ASC` - Least replies/comments first - `REPLIES_DESC` - Most replies/comments first - `VIEWS_ASC` - Least views first - `VIEWS_DESC` - Most views first - `REPOSTS_ASC` - Least reposts/retweets first - `REPOSTS_DESC` - Most reposts/retweets first - `QUOTES_ASC` - Least quotes first - `QUOTES_DESC` - Most quotes first - `SHARES_ASC` - Least shares first - `SHARES_DESC` - Most shares first - `CLICKS_ASC` - Least clicks first - `CLICKS_DESC` - Most clicks first - `PROFILE_CLICKS_ASC` - Least profile clicks first - `PROFILE_CLICKS_DESC` - Most profile clicks first - `REACH_ASC` - Least reach first - `REACH_DESC` - Most reach first - `TOTAL_ENGAGEMENTS_ASC` - Least total engagements first - `TOTAL_ENGAGEMENTS_DESC` - Most total engagements first - `ENGAGEMENT_RATE_ASC` - Lowest engagement rate first - `ENGAGEMENT_RATE_DESC` - Highest engagement rate first ::note When sorting by a metric that isn't available for a platform (e.g., sorting by `VIEWS_DESC` when including Twitter posts), those submissions will be ordered as if the value is 0. :: ## Response Structure The response includes: - **total**: The total number of submissions matching your filters (across all pages) - **objects**: An array of submission objects for the current page Each submission object includes: - **platformId**: The unique ID from the platform (e.g., Instagram post ID, Tweet ID) - **dateSubmitted**: When the post was published - **socialAccount**: Details about the account that published the post - **text**: The post text/caption - **thumbnailUrl**: URL to a thumbnail image (if available) - **url**: Direct link to the post on the platform - **target**: The target destination (e.g., subreddit name for Reddit posts) - **metrics**: Array of metric values specific to the platform - **postTags**: Array of tags applied to this post ## Examples ### Find Top Performing Posts Get the top 10 posts by likes across all accounts: ```json { "startDate": "2025-01-01T00:00:00Z", "endDate": "2025-12-31T23:59:59Z", "limit": 10, "orderBy": "LIKES_DESC" } ``` ### Search Product Mentions Find all posts mentioning a specific product: ```json { "socialAccountIds": ["123", "456"], "startDate": "2025-01-01T00:00:00Z", "endDate": "2025-03-31T23:59:59Z", "query": "iPhone 15", "limit": 50, "orderBy": "DATE_SUBMITTED_DESC" } ``` ### Client Monthly Report Generate a report of all posts for a specific client: ```json { "clientId": "client-001", "startDate": "2025-10-01T00:00:00Z", "endDate": "2025-10-31T23:59:59Z", "limit": 100, "orderBy": "DATE_SUBMITTED_DESC" } ``` ### Analyze High Engagement Posts Find posts with the highest engagement rate to analyze what works: ```json { "socialAccountIds": ["123", "456", "789"], "startDate": "2025-06-01T00:00:00Z", "endDate": "2025-08-31T23:59:59Z", "limit": 25, "orderBy": "ENGAGEMENT_RATE_DESC" } ``` ### Export All Posts Data Paginate through all posts to export data: ```json { "startDate": "2025-01-01T00:00:00Z", "endDate": "2025-12-31T23:59:59Z", "page": 1, "limit": 100, "orderBy": "DATE_SUBMITTED_DESC" } ``` # Platform Metrics Different social media platforms provide different types of analytics metrics. This guide lists all available metrics for each platform supported by Postpone. ## Reddit - **posts** - Number of posts submitted - **upvotes** - Total upvotes received - **comments** - Total comments received - **total_engagements** - Total engagements ## X/Twitter - **posts** - Number of tweets posted - **likes** - Total likes received - **replies** - Total replies received - **retweets** - Total retweets - **quote_tweets** - Total quote tweets - **impressions** - Total impressions - **url_link_clicks** - Total URL link clicks - **user_profile_clicks** - Total profile clicks - **total_engagements** - Total engagements - **engagement_rate** - Engagement rate ::note X/Twitter analytics only include posts submitted through Postpone. Posts created directly on X/Twitter are not included in analytics. :: ## Instagram - **posts** - Number of posts published - **likes** - Total likes received - **comments** - Total comments received - **follows** - Follows gained from posts - **impressions** - Total impressions - **shares** - Total shares - **profile_activity** - Profile activity generated - **profile_visits** - Profile visits from posts - **total_interactions** - Total interactions - **reach** - Total reach - **total_engagements** - Total engagements - **engagement_rate** - Engagement rate ## TikTok - **posts** - Number of videos posted - **likes** - Total likes received - **comments** - Total comments received - **shares** - Total shares - **impressions** - Total impressions - **total_engagements** - Total engagements - **engagement_rate** - Engagement rate ## Threads - **posts** - Number of threads posted - **likes** - Total likes received - **replies** - Total replies received - **views** - Total views - **reposts** - Total reposts - **quotes** - Total quote posts - **shares** - Total shares - **total_engagements** - Total engagements - **engagement_rate** - Engagement rate ## LinkedIn - **posts** - Number of posts published - **likes** - Total likes received - **comments** - Total comments received - **impressions** - Total impressions - **clicks** - Total clicks - **shares** - Total shares - **total_engagements** - Total engagements - **engagement_rate** - Engagement rate ## YouTube - **posts** - Number of videos published - **likes** - Total likes received - **dislikes** - Total dislikes received - **comments** - Total comments received - **views** - Total views - **favorites** - Total favorites - **total_engagements** - Total engagements - **engagement_rate** - Engagement rate ## Bluesky - **posts** - Number of posts published - **likes** - Total likes received - **replies** - Total replies received - **reposts** - Total reposts - **quotes** - Total quote posts - **total_engagements** - Total engagements ## Mastodon - **posts** - Number of posts published - **likes** - Total likes received - **replies** - Total replies received - **reposts** - Total reposts - **total_engagements** - Total engagements ## Facebook - **posts** - Number of posts published - **likes** - Total likes received - **comments** - Total comments received - **impressions** - Total impressions - **impressions_unique** - Unique impressions - **engaged_users** - Total engaged users - **engaged_fan** - Engaged fans - **video_views** - Total video views - **clicks** - Total clicks - **clicks_unique** - Unique clicks - **total_engagements** - Total engagements - **engagement_rate** - Engagement rate ## Pinterest - **posts** - Number of pins published - **reactions** - Total reactions received - **comments** - Total comments received - **impressions** - Total impressions - **pin_clicks** - Total pin clicks - **clickthroughs** - Total clickthroughs - **total_engagements** - Total engagements - **engagement_rate** - Engagement rate ## Tumblr - **posts** - Number of posts published - **notes** - Total notes (likes + reblogs + replies) - **total_engagements** - Total engagements ## Telegram - **posts** - Number of posts published - **reactions** - Total reactions received - **replies** - Total replies received in the linked discussion group - **total_engagements** - Total engagements ::note Telegram's Bot API does not expose view or forward counts. Reactions are only reported when the bot is an administrator of the channel or group, and replies are only counted when the bot is an administrator of the channel's discussion group. :: ## Platform-Agnostic Metrics When using queries that support multiple platforms simultaneously (like [Aggregate Social Account Post Metrics](https://developers.postpone.app/analytics/aggregate-social-account-post-metrics)), you can use these platform-agnostic metric names that automatically map to the appropriate platform-specific metric: - **likes** - Maps to likes (most platforms), upvotes (Reddit), notes (Tumblr), or reactions (Telegram) - **comments** - Maps to comments (most platforms) or replies (X/Twitter, Threads, Bluesky, Mastodon, Telegram) - **followers** - Returns the current follower count for the account - **impressions** - Available for platforms that track impressions - **views** - Available for platforms that track views - **shares** - Available for platforms that track shares - **reach** - Available for platforms that track reach ## Metric Availability Not all metrics are available for all platforms. When you request a metric that isn't supported by a platform, it will be excluded from the results or returned as `0` depending on the query. ::tip The `posts` metric is always available and represents the total number of posts published during the specified time period. :: # Introduction The Postpone Analytics API provides comprehensive metrics and insights about your social media posts across all connected platforms. ## Overview Our analytics queries allow you to: - **Track performance trends** over time with time-series data - **Compare metrics** across different platforms and accounts - **Analyze content categories** using post tags - **Search and filter** individual posts with detailed metrics - **Generate reports** with aggregated statistics ## Available Queries ::card-group :::card --- icon: i-lucide-trending-up title: Post Time Series Metrics to: https://developers.postpone.app/analytics/post-time-series-metrics --- Retrieve analytics metrics grouped by time periods (day, week, or month) to track performance trends. ::: :::card --- icon: i-lucide-bar-chart-3 title: Aggregate Post Metrics to: https://developers.postpone.app/analytics/aggregate-post-metrics --- Get total and average metrics across a time period for quick performance summaries. ::: :::card --- icon: i-lucide-users title: Aggregate Social Account Post Metrics to: https://developers.postpone.app/analytics/aggregate-social-account-post-metrics --- Compare performance across multiple social accounts with metrics broken down by account. ::: :::card --- icon: i-lucide-tag title: Post Tag Metrics to: https://developers.postpone.app/analytics/post-tag-metrics --- Analyze performance by content categories using post tags to identify what resonates. ::: :::card --- icon: i-lucide-list title: Multi-Platform Analytics Submissions to: https://developers.postpone.app/analytics/multi-platform-analytics-submissions --- Search and retrieve individual posts with detailed metrics across all platforms. ::: :::card --- icon: i-lucide-layout-grid title: Platform Metrics to: https://developers.postpone.app/analytics/platform-metrics --- Reference guide for all available metrics on each social media platform. ::: :: ## Platform Support Analytics are available for all supported platforms: - **Reddit** - Upvotes, comments, karma - **X/Twitter** - Likes, retweets, replies, impressions, engagement - **Instagram** - Likes, comments, impressions, reach, shares - **TikTok** - Likes, comments, shares, views - **Threads** - Likes, replies, views, reposts, quotes - **Facebook** - Likes, comments, impressions, video views, clicks - **LinkedIn** - Likes, comments, impressions - **YouTube** - Likes, dislikes, comments, views - **Bluesky** - Likes, replies, reposts, quotes - **Mastodon** - Likes, replies, reposts - **Tumblr** - Notes (likes + reblogs + replies) - **Telegram** - Reactions, replies ## Key Features ### Multi-Platform Support Most analytics queries support querying multiple platforms simultaneously, making it easy to get a unified view of your performance across all social media channels. ### Flexible Time Ranges All queries accept `startDate` and `endDate` parameters, allowing you to analyze any time period. Group data by day, week, or month to match your reporting needs. ### Advanced Filtering Filter your analytics by: - **Social accounts** - Query specific accounts or groups of accounts - **Campaigns** - Track performance of specific marketing campaigns - **Subreddits** - Filter Reddit posts by target communities - **Post tags** - Analyze content by category or theme - **Search terms** - Find posts containing specific keywords ### Rich Metrics Access comprehensive metrics including: - Engagement metrics (likes, comments, shares) - Reach metrics (impressions, views, reach) - Performance indicators (engagement rate, click-through rate) - Account growth (followers, subscribers) ## Common Use Cases ### Performance Dashboards Use [Post Time Series Metrics](https://developers.postpone.app/analytics/post-time-series-metrics) and [Aggregate Post Metrics](https://developers.postpone.app/analytics/aggregate-post-metrics) to build real-time performance dashboards showing trends and KPIs. ### Content Strategy Analyze [Post Tag Metrics](https://developers.postpone.app/analytics/post-tag-metrics) to discover which content themes perform best and inform your content planning. ### Account Comparison Compare performance across accounts using [Aggregate Social Account Post Metrics](https://developers.postpone.app/analytics/aggregate-social-account-post-metrics) to identify top performers. ### Campaign Reporting Generate comprehensive campaign reports by filtering queries with campaign IDs and analyzing performance across multiple dimensions. ### Content Audit Use [Multi-Platform Analytics Submissions](https://developers.postpone.app/analytics/multi-platform-analytics-submissions) to review individual posts, identify top performers, and discover optimization opportunities. ## Analytics History The amount of historical analytics data available depends on your subscription plan: - **Free Plan**: no analytics support - **Starter Plan**: 2 months of history - **Creator Plan**: 6 months of history - **Professional Plan**: 12 months of history - **Agency Plan**: Unlimited history ::note All analytics queries enforce the history limit for your plan. Requests for data outside your plan's history window will be automatically adjusted to the earliest available date. :: ## Getting Started 1. **Connect your accounts**: Make sure you have social accounts connected in Postpone 2. **Post content**: Analytics are generated for published posts 3. **Choose a query**: Select the analytics query that matches your needs 4. **Set time range**: Specify your desired start and end dates 5. **Request metrics**: Specify which metrics you want to retrieve ## Best Practices ### Optimize Query Performance - Request only the metrics you need - Use appropriate time ranges for your use case - Cache results when possible to reduce API calls - Use pagination for large result sets ### Data Freshness Analytics data is updated every 4-12 hours depending on the platform, except for X/Twitter, where data is updated twice (2 days and 7 days after post submission). Additionally, all platforms except for X/Twitter include posts submitted from outside of Postpone in Analytics. For X/Twitter, only posts submitted using Postpone are included in Analytics. ## Next Steps Explore the individual query documentation to learn more about specific analytics capabilities: - [Post Time Series Metrics](https://developers.postpone.app/analytics/post-time-series-metrics) - [Aggregate Post Metrics](https://developers.postpone.app/analytics/aggregate-post-metrics) - [Aggregate Social Account Post Metrics](https://developers.postpone.app/analytics/aggregate-social-account-post-metrics) - [Post Tag Metrics](https://developers.postpone.app/analytics/post-tag-metrics) - [Multi-Platform Analytics Submissions](https://developers.postpone.app/analytics/multi-platform-analytics-submissions) - [Platform Metrics](https://developers.postpone.app/analytics/platform-metrics)