Install
openclaw skills install postthatlaterSchedule and manage social media posts across multiple social platforms. Query analytics, manage your queue, and publish immediately — all via natural langua...
openclaw skills install postthatlaterSchedule and manage social media posts from Claude Code or any AI assistant.
Sign up at https://postthatlater.com — a subscription is required to use the API.
In PostThatLater: Account Settings → API Keys → Create new key
Copy the sk_ptl_... key — it's shown only once.
export PTL_API_KEY=sk_ptl_your_key_here
Add to ~/.zshrc or ~/.bashrc to persist across sessions.
curl https://postthatlater.com/api/v1/accounts \
-H "Authorization: Bearer $PTL_API_KEY"
You should see your connected social media accounts.
https://postthatlater.com
All API requests require:
Authorization: Bearer $PTL_API_KEY
Content-Type: application/json (for POST/PATCH)
GET /api/v1/accounts first — you need numeric account IDs to schedule posts. Never guess them.scheduled_at values must be ISO 8601 UTC (e.g. 2026-03-10T09:00:00Z). Convert from the user's local time.publish_now — publishing is immediate and irreversible. Always confirm with the user before calling the publish-now endpoint.Idempotency-Key on every POST/PATCH — generate a UUID per request to prevent duplicate posts on retry. Example: Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]').GET /api/v1/platforms to get the live list of available platforms and their limits. Never hardcode a platform list or assume a platform is available.account_ids to post the same content everywhere at once. The API creates one post record per account and links them via group_id.POST /api/v1/images with the image URL to get a filename, then pass that filename in the images array of POST /api/v1/posts. Use GET /api/v1/images to list already-stored images that can be reused without re-uploading.curl https://postthatlater.com/api/v1/accounts \
-H "Authorization: Bearer $PTL_API_KEY"
Response:
{
"data": [
{
"id": 12,
"platform": "bluesky",
"handle": "you.bsky.social",
"display_name": "you.bsky.social",
"status": "connected",
"created_at": "2026-01-10T08:30:00.000Z",
"updated_at": "2026-01-10T08:30:00.000Z"
}
],
"meta": { "request_id": "..." }
}
status is "connected" (healthy) or "error" (needs reconnecting in dashboard).
curl https://postthatlater.com/api/v1/accounts/12 \
-H "Authorization: Bearer $PTL_API_KEY"
curl "https://postthatlater.com/api/v1/posts?status=pending&limit=10&offset=0" \
-H "Authorization: Bearer $PTL_API_KEY"
Query parameters:
status — pending, posted, or failed (omit for all)platform — e.g. bluesky, mastodon, linkedinlimit — max results (default: 20, max: 100)offset — skip N results for pagination (default: 0)Response:
{
"data": [
{
"id": 101,
"text": "Hello from the API!",
"scheduled_at": "2026-03-15T09:00:00.000Z",
"status": "pending",
"platform": "bluesky",
"account_id": 12,
"images": [],
"platform_post_id": null,
"platform_post_url": null,
"error_message": null,
"retry_count": 0,
"likes": 0,
"comments": 0,
"shares": 0,
"group_id": null,
"created_at": "2026-03-01T12:00:00.000Z",
"updated_at": null
}
],
"meta": { "request_id": "...", "total": 5, "limit": 10, "offset": 0 }
}
curl -X POST https://postthatlater.com/api/v1/posts \
-H "Authorization: Bearer $PTL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
-d '{
"text": "Hello from the PostThatLater API!",
"scheduled_at": "2026-03-15T09:00:00.000Z",
"account_ids": [12, 15]
}'
Body parameters:
text (required) — post content; must not exceed platform char limitscheduled_at (required) — ISO 8601 UTC datetime; must be in the futureaccount_ids (required) — array of account IDs (at least one)images — optional array of image filenamesReturns HTTP 201 with the first created post. All posts share a group_id.
curl https://postthatlater.com/api/v1/posts/101 \
-H "Authorization: Bearer $PTL_API_KEY"
Only works on pending posts. Returns 409 conflict if already published or failed.
curl -X PATCH https://postthatlater.com/api/v1/posts/101 \
-H "Authorization: Bearer $PTL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
-d '{
"text": "Updated content for the post",
"scheduled_at": "2026-03-16T10:00:00.000Z"
}'
Cannot delete posted posts (returns 409). Failed posts can be deleted.
curl -X DELETE https://postthatlater.com/api/v1/posts/101 \
-H "Authorization: Bearer $PTL_API_KEY"
Response:
{ "data": { "deleted": true, "id": 101 }, "meta": { "request_id": "..." } }
Bypasses the scheduler. Post must be pending or failed. Irreversible — confirm with user first.
curl -X POST https://postthatlater.com/api/v1/posts/101/publish-now \
-H "Authorization: Bearer $PTL_API_KEY"
Response:
{
"data": {
"id": 101,
"status": "posted",
"platform_post_url": "https://bsky.app/profile/you.bsky.social/post/...",
...
},
"meta": { "request_id": "..." }
}
Post with images by first ingesting the image from a URL, then passing the returned filename when creating a post.
Downloads, processes (EXIF rotation, compression, format conversion), and stores the image server-side. Returns a filename to use in posts.
curl -X POST https://postthatlater.com/api/v1/images \
-H "Authorization: Bearer $PTL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/photo.jpg"}'
Response:
{
"data": { "filename": "1741036800000-123456789.jpeg" },
"meta": { "request_id": "..." }
}
Pass the filename in the images array of POST /api/v1/posts. Check GET /api/v1/platforms for the max_images limit per platform.
Returns all images referenced by your posts with file metadata and which post IDs use each image. Use this to reuse an already-uploaded image without re-ingesting.
curl https://postthatlater.com/api/v1/images \
-H "Authorization: Bearer $PTL_API_KEY"
Response:
{
"data": [
{
"filename": "1741036800000-123456789.jpeg",
"url": "/uploads/1741036800000-123456789.jpeg",
"size": 284672,
"created_at": "2026-03-01T09:00:00.000Z",
"referenced_by_post_ids": [101, 104]
}
],
"meta": { "request_id": "...", "total": 1 }
}
curl "https://postthatlater.com/api/v1/analytics/summary?period=30d" \
-H "Authorization: Bearer $PTL_API_KEY"
Query parameters: period — 7d, 30d, 90d, or all (default: 30d)
Response:
{
"data": {
"period": "30d",
"total_posts": 45,
"by_status": {
"posted": 40,
"pending": 3,
"failed": 2
},
"success_rate": 95.2,
"total_likes": 312,
"total_comments": 48,
"total_shares": 91,
"total_engagement": 451
},
"meta": { "request_id": "..." }
}
curl "https://postthatlater.com/api/v1/analytics/top-posts?period=30d&limit=5" \
-H "Authorization: Bearer $PTL_API_KEY"
Query parameters:
period — 7d, 30d, or all (default: 30d)limit — number of posts (default: 5, max: 20)curl https://postthatlater.com/api/v1/posts/101/metrics \
-H "Authorization: Bearer $PTL_API_KEY"
Response:
{
"data": {
"post_id": 101,
"platform": "bluesky",
"likes": 42,
"comments": 8,
"shares": 15,
"engagement": 65,
"published_at": "2026-03-01T09:00:00.000Z"
},
"meta": { "request_id": "..." }
}
curl "https://postthatlater.com/api/v1/analytics/by-platform?period=30d" \
-H "Authorization: Bearer $PTL_API_KEY"
curl "https://postthatlater.com/api/v1/analytics/timeline?period=7d" \
-H "Authorization: Bearer $PTL_API_KEY"
Public endpoint — no authentication required.
Returns character limits and capabilities for all currently available platforms. New platforms may be added over time, so always fetch this list rather than hardcoding platform names or limits.
curl https://postthatlater.com/api/v1/platforms
Response fields per platform:
name — slug used in API calls (e.g. bluesky)display_name — human-readable namechar_limit — maximum post length in charactersmax_images — maximum images per postsupports_video — whether video uploads are acceptednotes — platform-specific connection or usage notes
---
## Error Handling
All errors follow:
```json
{
"error": {
"code": "validation_error",
"message": "Validation failed.",
"details": [
{ "field": "text", "message": "Text exceeds Bluesky character limit (300 chars)." },
{ "field": "scheduled_at", "message": "scheduled_at must be a future datetime." }
]
}
}
| Code | HTTP | Meaning |
|---|---|---|
invalid_api_key | 401 | Missing, malformed, or revoked Bearer token |
subscription_required | 402 | No active subscription |
not_found | 404 | Resource doesn't exist or belongs to another user |
conflict | 409 | Action not allowed in current state (e.g. editing a published post) |
validation_error | 400 | Request body failed validation — check details array for field-level errors |
rate_limited | 429 | 60 req/min per key — check Retry-After header |
internal_error | 500 | Unexpected server error |
User: Schedule a post about our summer sale on Bluesky and Mastodon for tomorrow at 10am EST
Agent workflow:
1. GET /api/v1/accounts → find Bluesky (id: 12) and Mastodon (id: 15)
2. GET /api/v1/platforms → confirm char limits (300 / 500)
3. Convert 10am EST → 15:00 UTC (March 5 → 2026-03-05T15:00:00Z)
4. Confirm with user: "Ready to schedule 'Summer sale...' to Bluesky + Mastodon for Mar 5 at 10am EST?"
5. POST /api/v1/posts with account_ids: [12, 15] and Idempotency-Key header
6. Report back: "Scheduled! Post #101 and #102 will go out March 5 at 10am EST."