PostEsy / API reference

DEVELOPER REFERENCE

Posting API

Everything you need to create, schedule, and manage posts with PostEsy.

Your developer token

Sign in to create a token for posting requests. It expires after 15 minutes.

Sign in

This reference describes the posting and draft endpoints implemented by the PostEsy application. It is a reference for the current web API, not a promise of a separately supported public developer API. Connect the social accounts in PostEsy before publishing.

Getting started

Examples use the production API at https://api.post-esy.com with a POSIX shell and curl; replace every placeholder.

Get an access token

Sign in to PostEsy, open the API documentation page, and select Generate token. Copy the token into your terminal:

Example
export ACCESS_TOKEN="YOUR_DEVELOPER_TOKEN"

The developer token expires after 15 minutes. Generate another on the page when it expires. It is tied to the session used to create it: signing out of that session or ending the session also invalidates the token. Keep it private and do not commit or share it; entering it directly in a shell may save it in shell history. The token works only on the posting, job, draft, media, and connection-status routes described here. It does not grant access to other PostEsy APIs or create more developer tokens.

Send Authorization: Bearer <access token> with every protected request in this reference. The workspace is resolved from the authenticated session, not from a supplied user ID. Teammate permissions apply: reading needs view, editing drafts and staging media needs content, and publishing, rescheduling, cancelling, and deleting published posts need publish. Publishing also checks the subscription when billing enforcement is enabled.

MCP credentials are separate: an MCP API key or OAuth token is for /mcp and cannot be used as the bearer token for these web endpoints. See MCP server plan and the implementation for MCP details.

List connected accounts and their profile IDs:

Example
curl "https://api.post-esy.com/api/v1/auth/connections/status" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Endpoint overview

MethodPathPurpose
POST/api/v1/post/allQueue a post now or schedule it
GET/api/v1/post/jobsList publish jobs
GET/api/v1/post/jobs/statusesGet statuses for up to 50 jobs
GET/api/v1/post/jobs/{postId}Get one publish job
GET/api/v1/post/jobs/{postId}/media/{mediaIndex}Retrieve job media by zero-based index
PATCH/api/v1/post/jobs/{postId}/scheduleMove a queued scheduled submission
PATCH/api/v1/post/jobs/{postId}/cancelCancel a queued scheduled submission
DELETE/api/v1/post/jobs/{postId}/publishedDelete supported published platform posts for a job
DELETE/api/v1/post/jobs/{postId}/published/{platform}Delete the published post on one platform
POST/api/v1/post/media/presignPrepare direct media uploads
POST/api/v1/post/media/convertConvert an uploaded WebM to MP4
GET, POST/api/v1/draftsList or create drafts
PUT, DELETE/api/v1/drafts/{id}Replace or delete a draft
GET/api/v1/drafts/{id}/mediaGet temporary URLs for draft media

Publish a post

POST /api/v1/post/all accepts multipart form data. It creates one or more asynchronous publish jobs; a successful HTTP response means the request was queued, not that the social network has published it. Save data.postId and check its job status and platform results.

Example
curl -X POST "https://api.post-esy.com/api/v1/post/all" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -F 'content=Hello from PostEsy!' \
  -F 'postToLinkedIn=true' \
  -F 'linkedinProfileIds=["YOUR_LINKEDIN_PROFILE_UUID"]' \
  -F 'isScheduled=false'

The response has success, message, and data, including data.postId and data.jobId (the same primary job ID). Multiple selected accounts can create additional jobs for the submission; use history to inspect them. A job contains fields including status, platforms, platform_results, scheduled_time, and error_message.

Posting fields

Form values are strings. In particular, use the literal string true for flags. The request requires nonempty content and at least one platform flag. Profile IDs are PostEsy connected-profile UUIDs, not social network IDs; send the selected IDs as a JSON array encoded as a form field. For predictable account selection specify the IDs explicitly.

PlatformSelection flagProfile ID field
XpostToTwittertwitterProfileIds
FacebookpostToFacebookfacebookProfileIds
LinkedInpostToLinkedInlinkedinProfileIds
InstagrampostToInstagraminstagramProfileIds
ThreadspostToThreadsthreadsProfileIds
YouTubepostToYouTubeyoutubeProfileIds
BlueskypostToBlueskyblueskyProfileIds
PinterestpostToPinterestpinterestProfileIds
TelegrampostToTelegramtelegramProfileIds
TumblrpostToTumblrtumblrProfileIds
MastodonpostToMastodonmastodonProfileIds
SlackpostToSlackslackProfileIds
TikTokpostToTikToktiktokProfileIds

Optional fields include title, tags (a JSON-encoded array), mediaR2Keys (a JSON-encoded array of uploaded media references), and thumbnailR2Key (a JSON-encoded uploaded reference). You may alternatively attach local files as repeated media multipart fields. At most 10 media files and 200 MB total are allowed, with a configurable per-file limit and additional platform-specific checks. For example, Instagram requires exactly one image or video, YouTube requires a title and exactly one video, and TikTok requires media and an explicit tiktokPrivacyLevel. Pinterest requires account-specific board settings. Validation and provider requirements can vary by destination; see postnodebackend/src/posts/controller.js and docs/MEDIA_AND_CONTENT_LIMITS.md for the current rules.

Other platform-specific form fields include youtubePrivacyStatus, youtubeMadeForKids, youtubeCategoryId, instagramAspectRatio, tiktokPrivacyLevel, tiktokAllowComments, tiktokAllowDuet, tiktokAllowStitch, tiktokBrandContent, tiktokBrandOrganic, tiktokIsAigc, and the JSON maps pinterestBoardIdByProfile, pinterestLinkByProfile, pinterestAltTextByProfile, pinterestAiModifiedByProfile, and pinterestSyntheticPerformerByProfile.

To attach a local image to a compatible destination, add -F 'media=@./image.jpg' to the publish request. Media must meet the selected platforms' limits.

Schedule a post

Use the same endpoint with isScheduled=true, a future scheduleAt in ISO 8601 format, and an IANA timezone. An explicit offset or Z in scheduleAt makes the intended instant clear. As an alternative to scheduleAt, send scheduleDate (YYYY-MM-DD) and scheduleTime (HH:mm) with timezone; the server interprets them in that zone.

Example
curl -X POST "https://api.post-esy.com/api/v1/post/all" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -F 'content=Our next update' \
  -F 'postToLinkedIn=true' \
  -F 'linkedinProfileIds=["YOUR_LINKEDIN_PROFILE_UUID"]' \
  -F 'isScheduled=true' \
  -F 'scheduleAt=2030-06-15T09:00:00Z' \
  -F 'timezone=UTC'

Replace the example time with a future time. Scheduled submissions remain queued until the worker processes them.

Check and manage jobs

Example
export POST_ID="YOUR_POST_ID"

# Full details, including per-platform results
curl "https://api.post-esy.com/api/v1/post/jobs/$POST_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# History: page defaults to 1; pageSize defaults to 20 and is capped at 100
curl "https://api.post-esy.com/api/v1/post/jobs?page=1&pageSize=20" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Poll multiple IDs (1–50 comma-separated UUIDs); missing IDs are omitted
curl -G "https://api.post-esy.com/api/v1/post/jobs/statuses" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  --data-urlencode "ids=$POST_ID,YOUR_OTHER_POST_ID"

# Download the first media item from a job
curl "https://api.post-esy.com/api/v1/post/jobs/$POST_ID/media/0" \
  -H "Authorization: Bearer $ACCESS_TOKEN" -o media-download

History also accepts startDate and endDate as date-time values; they filter creation time, inclusive, not scheduled or published time. History returns data and pagination (page, pageSize, total, totalPages). Job statuses include queued, publishing, published, partial, failed, cancelled, and deleted. Inspect platform_results for individual outcomes; a partially successful job needs attention even if the initial request succeeded.

Reschedule or cancel only a scheduled job that is still queued. These actions affect the associated submission group, including its other account jobs; once publishing starts, the API returns a conflict.

Example
curl -X PATCH "https://api.post-esy.com/api/v1/post/jobs/$POST_ID/schedule" \
  -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-Type: application/json' \
  -d '{"scheduledTime":"2030-06-16T09:00:00Z"}'

curl -X PATCH "https://api.post-esy.com/api/v1/post/jobs/$POST_ID/cancel" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Rescheduling returns post_id, group_id, scheduled_time, and moved; cancellation returns post_id, group_id, and cancelled. The counts indicate how many jobs were affected.

To remove an already published post, use the job endpoint. The optional platform is a lowercase platform key such as linkedin.

Example
curl -X DELETE "https://api.post-esy.com/api/v1/post/jobs/$POST_ID/published/linkedin" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Or request deletion from all supported platforms in this job
curl -X DELETE "https://api.post-esy.com/api/v1/post/jobs/$POST_ID/published" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Platform deletion support varies. Check the response and per-platform outcomes: an HTTP 200 alone does not guarantee every platform deletion succeeded. These routes act on the specified job; other jobs in the submission may need separate requests.

The backend also retains legacy single-platform routes: POST /api/v1/deleteTweet/{id}, POST /api/v1/deleteLinkedInPost (JSON body { "postId": "..." }), GET /api/v1/gettweeterPosts, GET /api/v1/getlinkedinPosts, GET /api/v1/getUserDetailsTwitter/{userId}, and GET /api/v1/getUserDetailsLinkedin/{userId}. These legacy routes require a web session token and do not accept developer tokens. Their identifiers and response formats are platform-specific; the user ID on the detail routes must match the authenticated workspace. Prefer the job routes above for PostEsy publishing history and deletion.

Drafts

Drafts are saved workspace content, not queued jobs. Creating or updating one does not publish it. The web API does not expose a publish-by-draft-ID endpoint: load the draft, construct a /post/all request from its fields and media, then submit it. The draft remains saved until deleted.

Example
curl -X POST "https://api.post-esy.com/api/v1/drafts" \
  -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Launch idea","content":"Our new update","targets":{"linkedin":true}}'

curl "https://api.post-esy.com/api/v1/drafts?limit=25" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

export DRAFT_ID="YOUR_DRAFT_ID"
curl -X PUT "https://api.post-esy.com/api/v1/drafts/$DRAFT_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Launch idea","content":"Updated copy","targets":{"linkedin":true}}'

curl "https://api.post-esy.com/api/v1/drafts/$DRAFT_ID/media" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

curl -X DELETE "https://api.post-esy.com/api/v1/drafts/$DRAFT_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Create returns HTTP 201 and data containing the draft. name is required; a new draft must also have content, a selected target, or media. Supported request properties are name, content, postTitle, tags (string), targets, accountIdsByPlatform, platformOptions, and media (uploaded media references). PUT replaces the complete draft state: include all fields you want to keep. Name and post title are truncated to 120 characters, content to 20,000, and media to 10 entries. Lists use cursor pagination (limit, default 25, maximum 50; then pass pagination.nextCursor as cursor). Media URLs are temporary, and deleting a draft does not delete its reusable media. There is no individual GET /drafts/{id} route; use the list to find a draft.

Media uploads

For direct uploads, request signed URLs using the actual file size in bytes and MIME type. The returned data.uploads entries include key, uploadUrl, mimetype, originalname, size, and type. Upload each file with HTTP PUT using the signed content type and actual content length; do not send the PostEsy bearer token to the storage URL. The URLs expire (normally after 900 seconds).

Example
# Replace size with the exact byte length of image.jpg.
curl -X POST "https://api.post-esy.com/api/v1/post/media/presign" \
  -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-Type: application/json' \
  -d '{"files":[{"name":"image.jpg","type":"image/jpeg","size":12345}]}'

curl -X PUT 'YOUR_RETURNED_UPLOAD_URL' \
  -H 'Content-Type: image/jpeg' --data-binary '@image.jpg'

After uploading, pass the returned reference (at least key, mimetype, originalname, and size) in a draft's media array or as the JSON-encoded mediaR2Keys form field on /post/all. References must belong to the current workspace and the server verifies the uploaded object. Do not use an arbitrary external URL or a presigned URL in place of the object key.

For a workspace-owned uploaded WebM, POST /api/v1/post/media/convert accepts JSON { "key": "...", "mimetype": "video/webm" } and returns a new MP4 reference under data. The original upload is not the returned MP4.

Bulk import

The dashboard's bulk import reads rows on the client and submits each one separately to POST /api/v1/post/all; there is currently no CSV upload or bulk REST endpoint. For a script, parse your file locally, send one request per row using the publish or schedule format above, record each returned ID, and handle failures per row. Requests can succeed independently, so a later failure does not undo earlier jobs. Avoid blindly retrying an uncertain request: the web route does not provide an idempotency key, and another request can create a duplicate post.

PostEsy also implements an MCP bulk_schedule_posts tool at /mcp, using MCP authentication and JSON-RPC rather than this REST request format. It accepts up to 25 items, validates the set before sending, and returns individual outcomes; errors during sending can still result in partial success. MCP also has publish_draft. These are MCP tools, not REST routes.

Errors and operational notes

Responses commonly contain message, sometimes with success: false, errors, or error_code; shapes vary by endpoint. Common statuses include 400 for invalid input, 401 for a missing, expired, or ended session, 403 for insufficient access or plan restrictions, 404 for a missing resource, 409 for a state conflict, 413 for oversized media, 429 for rate limiting, and 500 for a server error. Inspect the response body and rate-limit headers where provided. Do not assume every failure has the same envelope or that all validation errors use the same status.

These routes currently use short-lived developer tokens tied to a signed-in session. A broader public developer API would need a deliberate contract for long-running integrations, including durable scoped credentials, stable versioned schemas, predictable error and pagination formats, published limits, idempotency for writes, and a documented bulk contract.