Developers
Lunar Post API and MCP docs
Schedule videos and images to your customers' social accounts from your own code, or let Claude and Cursor do it. One API key works for the REST API and the MCP server.
Base URLhttps://post.lnr.ae/api/post/v1
On this page
Overview
Lunar Post has a REST API for your servers and an MCP server for AI tools like Claude and Cursor. Both act in the workspace the API key belongs to, and share its plan, profiles and connected accounts.
- REST API
- https://post.lnr.ae/api/post/v1
- MCP server
- https://post.lnr.ae/api/post/mcp
What you can do
- Profiles and accountsA profile is one of your customers. Each has its own connected accounts, and posts never cross between profiles.
- MediaUpload a video or image, or hand over a link to one. Every file is checked before it can be posted.
- PostsSchedule one media item to a profile's accounts with a caption per network, then follow and change the posts.
- UsageWhere the workspace stands against its Lunar Post plan this month.
Quickstart
Get a key
Open the dashboard, go to API keys and create one. Copy it straight away: it's shown once.
Check your plan
Ask for this month's usage. If the key works, you get your plan and the posts you have left.
curl https://post.lnr.ae/api/post/v1/usage \ -H "Authorization: Bearer YOUR_API_KEY"Add a customer and post
Create a profile, send its connect link, upload media and schedule it. Every endpoint is in the API reference below, with an example to copy.
Authentication
Send the key in the Authorization header of every request, as a bearer token.
Authorization: Bearer YOUR_API_KEYEach key is read only, read and write, or (made by the workspace owner) read, write and approve. Every endpoint and tool says which scope it needs.
Treat a key like a password. Keep it on your server or in your AI tool's config, never in a web page or a public repository. Revoke it from the dashboard the moment you think it's leaked.
Connect Claude or Cursor
The MCP server lets an AI tool use Lunar Post for you: it lists profiles, uploads media and schedules posts when you ask. Replace YOUR_API_KEY with your key.
Claude Code
Run this once in your terminal. Claude Code can then use Lunar Post in every project.
claude mcp add --transport http lunar-post \
https://post.lnr.ae/api/post/mcp \
--header "Authorization: Bearer YOUR_API_KEY"Claude Desktop
Open Settings, Developer, Edit config in Claude Desktop, add this to claude_desktop_config.json and restart Claude. Claude Desktop reaches the server through mcp-remote, so it needs Node.js installed.
{
"mcpServers": {
"lunar-post": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://post.lnr.ae/api/post/mcp",
"--header",
"Authorization:${LUNAR_POST_AUTH}"
],
"env": {
"LUNAR_POST_AUTH": "Bearer YOUR_API_KEY"
}
}
}
}Cursor
Add this to ~/.cursor/mcp.json for every project, or to .cursor/mcp.json in one project.
{
"mcpServers": {
"lunar-post": {
"url": "https://post.lnr.ae/api/post/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}Other MCP clients
Any client that supports MCP over Streamable HTTP can connect. Give it the server URL and send the key as a bearer token.
URL: https://post.lnr.ae/api/post/mcp
Header: Authorization: Bearer YOUR_API_KEYConnect a customer's accounts
You never connect a customer's accounts yourself. Create a connect link for their profile and send them to it: they sign in on each network's own page, then Lunar Post sends them back to your site.
Create a profile for the customer, once. Put your own id for them in
externalRef.Create a connect link with a
returnUrlon your site. It works for an hour, or setttlSecondsfrom 5 minutes to 24 hours.Send the customer to the link. When they finish, they land on your
returnUrlwithstatusset toconnected,cancelledorfailed, and the profile's id inprofile.List the profile's accounts to get the ids you schedule posts to.
curl -X POST https://post.lnr.ae/api/post/v1/profiles/PROFILE_ID/connect-links \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "returnUrl": "https://app.example.com/connected", "networks": ["instagram", "tiktok"] }'
# → { "data": { "url": "https://post.lnr.ae/connect/…", "expiresAt": "…" }, "error": null }Anyone holding the link can connect accounts to that profile until it expires. Send it only to the customer.
MCP tools
The MCP server has 18 tools. Your AI tool picks the right one from what you ask, so you rarely name them yourself.
get_usageReadGet usage. Get the workspace's Lunar Post plan, this month's posts used against its limit (one post per account posted to, UTC calendar month) and live profiles against the plan's profiles.
list_profilesReadList profiles. List the workspace's profiles, newest first.
create_profileAsks firstWriteCreate profile. Create a profile for one of the developer's customers.
get_profileReadGet profile. Get one profile with its accounts: each account's id, network, handle and status (connected, needs_reauth or disconnected; only connected accounts can post).
create_connect_linkWriteCreate connect link. Make a short-lived link (1 hour by default) to a Lunar Post page where the profile's owner connects their social accounts by signing in on each network's own page.
list_accountsReadList accounts. List a profile's connected accounts: the ids schedule_post takes in accountIds, with network, handle, status and timezone.
create_upload_urlWriteCreate upload URL. Start uploading a video (MP4 or MOV, up to 1 GB) or image (JPEG, PNG or WebP, up to 20 MB) you have as a file.
complete_uploadWriteComplete upload. Finish an upload after the PUT: checks the stored file's size and type, saves it as media and starts its content check.
import_media_from_urlWriteImport media from a link. Fetch a video or image from a public https link, check its real type and size, save it as media and start its content check.
list_mediaReadList media. List the workspace's Lunar Post media, newest first, with each item's id, kind, profile and contentCheck (pending, passed or held; only passed media can be posted).
schedule_postAsks firstWriteSchedule post. Schedule one media item to one or more of a profile's accounts, with a caption per network.
publish_nowAsks firstWritePublish now. Post one media item to one or more of a profile's accounts right away, with a caption per network.
list_postsReadList posts. List Lunar Post posts, latest scheduled first, with status, time, live link (externalUrl) and failureReason.
get_postReadGet post. Get one post: its status (scheduled, publishing, published, failed…), when it goes or went out, the live link once published and the reason if it failed.
update_postAsks firstWriteUpdate post. Move a post that hasn't gone out to a new time and/or change its caption and hashtags.
cancel_postAsks firstWriteCancel post. Cancel a post that hasn't gone out.
retry_postAsks firstWriteRetry post. Send a failed post again (it goes out in about 3 minutes).
approve_postAsks firstApproveApprove post. Approve a post waiting for the workspace owner's review, which schedules it.
Asks firstShows a preview and waits for your OK before it changes anything.
Plans and limits
Limits are hard: past them, the request is refused with plan_limit and nothing extra is charged. One post is one account posted to, counted by UTC calendar month.
| Plan | Posts a month | Profiles |
|---|---|---|
| Trial | 50 | 3 |
| Starter | 500 | 10 |
| Growth | 2,500 | 50 |
| Scale | 10,000 | 250 |
Each key can make 60 requests a minute. Past that you get rate_limited (HTTP 429) with a Retry-After header saying how many seconds to wait.
Responses and errors
Every response is JSON with two fields, data and error. On success, data holds the result and error is null. On failure it's the other way round, and error has a code to act on and a message you can show a person.
Success
{
"data": {
"posts": {
"used": 12,
"limit": 50
},
"profiles": {
"used": 2,
"limit": 3
}
},
"error": null
}Failure
{
"data": null,
"error": {
"code": "plan_limit",
"message": "This workspace has used its 50 posts for this month on the Trial plan. Posting opens again on the 1st (UTC)."
}
}| Code | HTTP status | What it means |
|---|---|---|
invalid_input | 400 | A field is missing or out of range. The message says which. |
plan_limit | 403 | The month's posts, the plan's profiles or its media storage are used up, or the trial has ended. |
rate_limited | 429 | Too many requests from this key. Wait for the Retry-After seconds. |
unauthorized | 401 | The key is missing, malformed, revoked or expired. |
forbidden | 403 | The key can't do this: a read-only key tried to write, or it lacks the approve scope. |
not_found | 404 | Nothing with that id in this workspace. |
invalid_json | 400 | The body isn't valid JSON. |
internal | 500 | Something failed on our side. Try again; if it keeps happening, tell us. |
OpenAPI
The whole REST API is described in one OpenAPI 3.1 file. Import it into Postman or Insomnia, give it to Cursor or another AI tool as context, or generate a client in your language.
curl -o lunar-post-openapi.json https://post.lnr.ae/api/post/v1/openapi.jsonAPI reference
Profiles and accounts
A profile is one of your customers. Each has its own connected accounts, and posts never cross between profiles.
/api/post/v1/profilesList profiles
ReadThe workspace's live profiles, newest first. Find one by your own id with `externalRef`; add `includeArchived=true` to see archived ones too.
MCP tool: list_profiles
Parameters
externalRefstringQueryOptionalOnly the profile with this externalRef. (at most 200 characters)
includeArchivedbooleanQueryOptionalInclude archived profiles. Default false.
limitintegerQueryOptionalHow many to return. Default 100. (1–250)
Example request
curl https://post.lnr.ae/api/post/v1/profiles \
-H "Authorization: Bearer YOUR_API_KEY"Example response
HTTP 200{
"data": {
"profiles": [
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "Acme Coffee",
"externalRef": "cus_8841",
"createdAt": "2026-11-14T09:12:00.000Z",
"archivedAt": null
}
]
},
"error": null
}/api/post/v1/profilesCreate a profile
WriteAdds a profile for one of your customers. Uses one of the plan's profiles, and is refused with `plan_limit` when they're all in use (archive one to free it).
MCP tool: create_profile
Parameters
preview"1"QueryOptional`1` runs every check and returns `{ preview }` (what would happen) without changing anything.
Body
namestringRequiredThe customer's name, shown on the connect page. (1–80 characters)
externalRefstring | nullOptionalYour own id for this customer. Unique among live profiles; null clears it. (at most 200 characters)
Example request
curl -X POST https://post.lnr.ae/api/post/v1/profiles \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Coffee",
"externalRef": "cus_8841"
}'Example response
HTTP 201{
"data": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "Acme Coffee",
"externalRef": "cus_8841",
"createdAt": "2026-11-14T09:12:00.000Z",
"archivedAt": null
},
"error": null
}/api/post/v1/profiles/{id}Get a profile
ReadOne profile with its accounts. Only accounts with status `connected` can post; `needs_reauth` means the customer has to connect it again.
MCP tool: get_profile
Parameters
idstring (uuid)PathRequiredThe profile's id, from List profiles or Create a profile.
Example request
curl https://post.lnr.ae/api/post/v1/profiles/ID \
-H "Authorization: Bearer YOUR_API_KEY"Example response
HTTP 200{
"data": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "Acme Coffee",
"externalRef": "cus_8841",
"createdAt": "2026-11-14T09:12:00.000Z",
"archivedAt": null,
"accounts": [
{
"id": "c4a1e2f3-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"platform": "instagram",
"handle": "acmecoffee",
"avatarUrl": "https://cdn.example.com/avatars/acme.jpg",
"status": "connected",
"timezone": "Asia/Dubai"
},
{
"id": "d7e8f9a0-1b2c-4d3e-8f4a-5b6c7d8e9f0a",
"platform": "tiktok",
"handle": "acme.coffee",
"avatarUrl": null,
"status": "connected",
"timezone": "Asia/Dubai"
}
]
},
"error": null
}/api/post/v1/profiles/{id}Update a profile
WriteRenames a live profile, or changes or clears its externalRef. Send at least one of the two.
Parameters
idstring (uuid)PathRequiredThe profile's id, from List profiles or Create a profile.
Body
namestringOptionalThe customer's name, shown on the connect page. (1–80 characters)
externalRefstring | nullOptionalYour own id for this customer. Unique among live profiles; null clears it. (at most 200 characters)
Example request
curl -X PATCH https://post.lnr.ae/api/post/v1/profiles/ID \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Coffee Roasters"
}'Example response
HTTP 200{
"data": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "Acme Coffee Roasters",
"externalRef": "cus_8841",
"createdAt": "2026-11-14T09:12:00.000Z",
"archivedAt": null
},
"error": null
}/api/post/v1/profiles/{id}/archiveArchive a profile
WriteArchives a profile so it frees one of the plan's profiles. Refused while it has posts waiting to go out: cancel them first. Archiving an archived profile changes nothing.
Parameters
idstring (uuid)PathRequiredThe profile's id, from List profiles or Create a profile.
Example request
curl -X POST https://post.lnr.ae/api/post/v1/profiles/ID/archive \
-H "Authorization: Bearer YOUR_API_KEY"Example response
HTTP 200{
"data": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "Acme Coffee",
"externalRef": "cus_8841",
"createdAt": "2026-11-14T09:12:00.000Z",
"archivedAt": "2026-11-18T12:00:00.000Z"
},
"error": null
}/api/post/v1/profiles/{id}/accountsThe profile's connected accounts: the ids Schedule a post takes in `accountIds`, with network, handle, status and timezone.
MCP tool: list_accounts
Parameters
idstring (uuid)PathRequiredThe profile's id, from List profiles or Create a profile.
Example request
curl https://post.lnr.ae/api/post/v1/profiles/ID/accounts \
-H "Authorization: Bearer YOUR_API_KEY"Example response
HTTP 200{
"data": {
"accounts": [
{
"id": "c4a1e2f3-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"platform": "instagram",
"handle": "acmecoffee",
"avatarUrl": "https://cdn.example.com/avatars/acme.jpg",
"status": "connected",
"timezone": "Asia/Dubai"
},
{
"id": "d7e8f9a0-1b2c-4d3e-8f4a-5b6c7d8e9f0a",
"platform": "tiktok",
"handle": "acme.coffee",
"avatarUrl": null,
"status": "connected",
"timezone": "Asia/Dubai"
}
]
},
"error": null
}/api/post/v1/profiles/{id}/connect-linksA short-lived link to a Lunar Post page where your customer connects their accounts, signing in on each network's own page. Send them there; when they finish they go to `returnUrl` with `?status=connected|cancelled|failed&profile=<id>`. Anyone holding the link can connect accounts to this profile until it expires.
MCP tool: create_connect_link
Parameters
idstring (uuid)PathRequiredThe profile's id, from List profiles or Create a profile.
Body
returnUrlstringRequiredFull https address on a public host name. (at most 2000 characters)
networksarray of "instagram" | "tiktok" | "linkedin" | "facebook" | "threads" | "youtube" | "pinterest"OptionalNetworks the page offers. All of them when left out. (at most 10 items)
ttlSecondsintegerOptionalHow long the link works. Default 3600 (an hour). (300–86400)
Example request
curl -X POST https://post.lnr.ae/api/post/v1/profiles/ID/connect-links \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"returnUrl": "https://app.example.com/settings/social?customer=cus_8841",
"networks": [
"instagram",
"tiktok"
]
}'Example response
HTTP 201{
"data": {
"url": "https://post.lnr.ae/connect/eyJ2IjoxLCJ3b3Jrc3BhY2VJZCI6Ii4uLiJ9.7c9e6679",
"expiresAt": "2026-11-14T11:12:00.000Z"
},
"error": null
}Media
Upload a video or image, or hand over a link to one. Every file is checked before it can be posted.
/api/post/v1/media/uploadsCreate an upload URL
WriteStep one of an upload: returns a signed `uploadUrl`, the `headers` to send and the `storagePath`. PUT the file's bytes to `uploadUrl` with those headers within 2 hours, then call Complete an upload. Videos: MP4 or MOV up to 1 GB. Images: JPEG, PNG or WebP up to 20 MB.
MCP tool: create_upload_url
Body
fileNamestringRequiredWith its extension, e.g. launch.mp4. (1–200 characters)
contentType"video/mp4" | "video/quicktime" | "image/jpeg" | "image/png" | "image/webp"RequiredsizeBytesintegerRequiredThe file's exact size in bytes. (at least 1)
profileIdstring | null (uuid)OptionalFile it under this profile (only its posts can use it). Leave out to share it across your profiles.
Example request
curl -X POST https://post.lnr.ae/api/post/v1/media/uploads \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fileName": "cold-brew-launch.mp4",
"contentType": "video/mp4",
"sizeBytes": 48211904,
"profileId": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}'Example response
HTTP 201{
"data": {
"storagePath": "1b4e28ba-2fa1-41d2-883f-0016d3cca427/post/7c9e6679-7425-40de-944b-e07fc1f90ae7/0f7e5c1a-3b2d-4e6f-8a9b-1c2d3e4f5a6b-cold-brew-launch.mp4",
"uploadUrl": "https://storage.example.com/object/upload/sign/media/…?token=…",
"method": "PUT",
"headers": {
"Content-Type": "video/mp4"
},
"expiresAt": "2026-11-14T12:02:00.000Z",
"kind": "video"
},
"error": null
}/api/post/v1/media/uploads/completeComplete an upload
WriteStep two, after the PUT: checks the stored file's real size and type (removing it if they don't match what you declared), saves it as media and starts its content check. `contentCheck` stays `pending` until the check is done, usually under a minute.
MCP tool: complete_upload
Body
storagePathstringRequired(at most 400 characters)
sizeBytesintegerRequired(at least 1)
titlestringOptionalA name for the media; also the YouTube title when the post has none. (at most 200 characters)
Example request
curl -X POST https://post.lnr.ae/api/post/v1/media/uploads/complete \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"storagePath": "1b4e28ba-2fa1-41d2-883f-0016d3cca427/post/7c9e6679-7425-40de-944b-e07fc1f90ae7/0f7e5c1a-3b2d-4e6f-8a9b-1c2d3e4f5a6b-cold-brew-launch.mp4",
"sizeBytes": 48211904,
"title": "Cold brew launch"
}'Example response
HTTP 201{
"data": {
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"profileId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"kind": "video",
"source": "upload",
"name": "cold-brew-launch.mp4",
"title": "Cold brew launch",
"createdAt": "2026-11-14T10:02:00.000Z",
"contentCheck": "pending"
},
"error": null
}/api/post/v1/media/importImport from a link
WriteFetches a video or image from a public https link, reads its real type from the file itself, caps its size while it downloads, saves it as media and starts its content check. Links to private or local addresses are refused.
MCP tool: import_media_from_url
Body
urlstringRequiredPublic https link to the file itself. (at most 2000 characters)
profileIdstring | null (uuid)OptionalFile it under this profile (only its posts can use it). Leave out to share it across your profiles.
titlestringOptionalA name for the media; also the YouTube title when the post has none. (at most 200 characters)
fileNamestringOptionalName to store it under. Taken from the link when left out. (at most 200 characters)
Example request
curl -X POST https://post.lnr.ae/api/post/v1/media/import \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://cdn.example.com/renders/cold-brew-launch.mp4",
"profileId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"title": "Cold brew launch"
}'Example response
HTTP 201{
"data": {
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"profileId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"kind": "video",
"source": "link",
"name": "cold-brew-launch.mp4",
"title": "Cold brew launch",
"createdAt": "2026-11-14T10:02:00.000Z",
"contentCheck": "pending"
},
"error": null
}/api/post/v1/mediaList media
ReadThe workspace's Lunar Post media, newest first, with each item's `contentCheck`: `pending`, `passed` (can be posted) or `held` (waiting for a content review, can't be posted). Pages with `nextCursor`.
MCP tool: list_media
Parameters
profileIdstringQueryOptionalA profile id, or `shared` for media shared across profiles.
kind"video" | "image"QueryOptionalOnly videos or only images.
limitintegerQueryOptionalHow many to return. Default 20. (1–100)
cursorstringQueryOptionalThe nextCursor from the previous page.
Example request
curl https://post.lnr.ae/api/post/v1/media \
-H "Authorization: Bearer YOUR_API_KEY"Example response
HTTP 200{
"data": {
"media": [
{
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"profileId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"kind": "video",
"source": "upload",
"name": "cold-brew-launch.mp4",
"title": "Cold brew launch",
"createdAt": "2026-11-14T10:02:00.000Z",
"contentCheck": "passed"
}
],
"nextCursor": null
},
"error": null
}/api/post/v1/media/{id}Get a media item
ReadOne media item with its content check. Poll it after an upload until `contentCheck` is `passed`.
Parameters
idstring (uuid)PathRequiredThe media item's id, from Complete an upload, Import from a link or List media.
Example request
curl https://post.lnr.ae/api/post/v1/media/ID \
-H "Authorization: Bearer YOUR_API_KEY"Example response
HTTP 200{
"data": {
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"profileId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"kind": "video",
"source": "upload",
"name": "cold-brew-launch.mp4",
"title": "Cold brew launch",
"createdAt": "2026-11-14T10:02:00.000Z",
"contentCheck": "passed"
},
"error": null
}/api/post/v1/media/{id}Delete a media item
WriteDeletes the file. Refused while posts using it are waiting to go out: cancel them first. Posts that already went out keep their history.
Parameters
idstring (uuid)PathRequiredThe media item's id, from Complete an upload, Import from a link or List media.
Example request
curl -X DELETE https://post.lnr.ae/api/post/v1/media/ID \
-H "Authorization: Bearer YOUR_API_KEY"Example response
HTTP 200{
"data": {
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"deleted": true
},
"error": null
}Posts
Schedule one media item to a profile's accounts with a caption per network, then follow and change the posts.
/api/post/v1/postsSchedule a post
WritePosts one media item to one or more of a profile's accounts: `when` is a time, `"now"` or `"next_free_slot"`. Each account uses one of this month's posts, reserved before anything is written and refused with `plan_limit` at the limit. Keys act as editors: in a workspace that reviews posts, they wait for the owner's approval (`awaitingApproval`).
MCP tool: schedule_post
Parameters
preview"1"QueryOptional`1` runs every check and returns `{ preview }` (what would happen) without changing anything.
Body
mediaIdstring (uuid)RequiredMedia whose content check passed.
accountIdsarray of string (uuid)RequiredAll from the same profile. Each account counts as one post. (1–25 items)
whenstringRequired"now", "next_free_slot" (each account's next posting time) or an ISO 8601 time with offset at least 2 minutes ahead.
profileIdstring | null (uuid)OptionalThe profile whose accounts you post to. Null or absent: the workspace's own accounts.
captionsobjectOptionalOne caption per network.
captionobjectOptionalThe caption for every network without an entry in captions.
titlestringOptionalYouTube and Pinterest title. (at most 100 characters)
sendForReviewbooleanOptionalHold the posts for the workspace owner's approval.
aiGeneratedbooleanOptionalLabel the post as AI-generated where the network supports it (YouTube, TikTok).
tiktokobjectOptionalyoutubeobjectOptional
Example request
curl -X POST https://post.lnr.ae/api/post/v1/posts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mediaId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"profileId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"accountIds": [
"c4a1e2f3-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"d7e8f9a0-1b2c-4d3e-8f4a-5b6c7d8e9f0a"
],
"captions": {
"instagram": {
"caption": "Cold brew is back. Twelve hours steeped, zero bitterness.",
"hashtags": [
"coldbrew",
"acmecoffee"
]
},
"tiktok": {
"caption": "POV: your coffee took 12 hours so you don'\''t have to wait",
"hashtags": [
"coldbrew"
]
}
},
"when": "2026-11-20T09:00:00+04:00",
"aiGenerated": true
}'Example response
HTTP 201{
"data": {
"postIds": [
"3f8f6c1e-2b4a-4c3d-9e5f-1a2b3c4d5e6f",
"8b2d4f6a-1c3e-4a5b-9d7f-0e2a4c6b8d1f"
],
"awaitingApproval": false,
"usage": {
"used": 14,
"limit": 50
}
},
"error": null
}/api/post/v1/postsList posts
ReadLunar Post posts, latest scheduled first, with status, live link and failure reason. Filter by profile, account, network, status and a window on the scheduled time.
MCP tool: list_posts
Parameters
profileIdstringQueryOptionalA profile id, or `unprofiled` for the workspace's own accounts.
accountIdstring (uuid)QueryOptionalOnly this account's posts.
platform"instagram" | "tiktok" | "linkedin" | "facebook" | "threads" | "youtube" | "pinterest"QueryOptionalOnly this network.
status"draft" | "ready" | "approved" | "scheduled" | "publishing" | "awaiting_creator" | "published" | "failed" | "cancelled"QueryOptionalOnly posts in this status.
fromstring (date-time)QueryOptionalScheduled at or after (ISO 8601 with offset).
tostring (date-time)QueryOptionalScheduled before (ISO 8601 with offset).
limitintegerQueryOptionalHow many to return. Default 20. (1–100)
Example request
curl https://post.lnr.ae/api/post/v1/posts \
-H "Authorization: Bearer YOUR_API_KEY"Example response
HTTP 200{
"data": {
"posts": [
{
"id": "3f8f6c1e-2b4a-4c3d-9e5f-1a2b3c4d5e6f",
"platform": "instagram",
"accountId": "c4a1e2f3-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"status": "scheduled",
"caption": "Cold brew is back. Twelve hours steeped, zero bitterness.",
"hashtags": [
"coldbrew",
"acmecoffee"
],
"scheduledFor": "2026-11-20T05:00:00.000Z",
"publishedAt": null,
"externalUrl": null,
"failureReason": null,
"videoId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"mediaId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"profileId": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
]
},
"error": null
}/api/post/v1/posts/{id}Get a post
ReadOne post: its status, when it goes or went out, the live link once published (`externalUrl`) and `failureReason` if it failed.
MCP tool: get_post
Parameters
idstring (uuid)PathRequiredThe post's id, from Schedule a post (postIds) or List posts.
Example request
curl https://post.lnr.ae/api/post/v1/posts/ID \
-H "Authorization: Bearer YOUR_API_KEY"Example response
HTTP 200{
"data": {
"id": "3f8f6c1e-2b4a-4c3d-9e5f-1a2b3c4d5e6f",
"platform": "instagram",
"accountId": "c4a1e2f3-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"status": "published",
"caption": "Cold brew is back. Twelve hours steeped, zero bitterness.",
"hashtags": [
"coldbrew",
"acmecoffee"
],
"scheduledFor": "2026-11-20T05:00:00.000Z",
"publishedAt": "2026-11-20T05:00:41.000Z",
"externalUrl": "https://www.instagram.com/reel/C9xYz12AbCd/",
"failureReason": null,
"videoId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"mediaId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"profileId": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
},
"error": null
}/api/post/v1/posts/{id}Update a post
WriteMoves a post that hasn't gone out and/or changes its caption and hashtags. In a workspace that reviews posts, changing an approved post sends it back for approval.
MCP tool: update_post
Parameters
idstring (uuid)PathRequiredThe post's id, from Schedule a post (postIds) or List posts.
preview"1"QueryOptional`1` runs every check and returns `{ preview }` (what would happen) without changing anything.
Body
whenstringOptionalISO 8601 with offset, at least 2 minutes ahead.
captionstringOptional(at most 5000 characters)
hashtagsarray of stringOptional(at most 30 items)
Example request
curl -X PATCH https://post.lnr.ae/api/post/v1/posts/ID \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"when": "2026-11-21T09:00:00+04:00"
}'Example response
HTTP 200{
"data": {
"id": "3f8f6c1e-2b4a-4c3d-9e5f-1a2b3c4d5e6f",
"platform": "instagram",
"accountId": "c4a1e2f3-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"status": "scheduled",
"caption": "Cold brew is back. Twelve hours steeped, zero bitterness.",
"hashtags": [
"coldbrew",
"acmecoffee"
],
"scheduledFor": "2026-11-21T05:00:00.000Z",
"publishedAt": null,
"externalUrl": null,
"failureReason": null,
"videoId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"mediaId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"profileId": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
},
"error": null
}/api/post/v1/posts/{id}Cancel a post
WriteCancels a post that hasn't gone out. It won't be posted, and the post it used comes back to its month's allowance.
MCP tool: cancel_post
Parameters
idstring (uuid)PathRequiredThe post's id, from Schedule a post (postIds) or List posts.
preview"1"QueryOptional`1` runs every check and returns `{ preview }` (what would happen) without changing anything.
Example request
curl -X DELETE https://post.lnr.ae/api/post/v1/posts/ID \
-H "Authorization: Bearer YOUR_API_KEY"Example response
HTTP 200{
"data": {
"id": "3f8f6c1e-2b4a-4c3d-9e5f-1a2b3c4d5e6f",
"platform": "instagram",
"accountId": "c4a1e2f3-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"status": "cancelled",
"caption": "Cold brew is back. Twelve hours steeped, zero bitterness.",
"hashtags": [
"coldbrew",
"acmecoffee"
],
"scheduledFor": "2026-11-20T05:00:00.000Z",
"publishedAt": null,
"externalUrl": null,
"failureReason": null,
"videoId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"mediaId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"profileId": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
},
"error": null
}/api/post/v1/posts/{id}/retryRetry a post
WriteSends a failed post again, about 3 minutes later. If the failure reason says the account needs reconnecting, send the customer a new connect link first.
MCP tool: retry_post
Parameters
idstring (uuid)PathRequiredThe post's id, from Schedule a post (postIds) or List posts.
preview"1"QueryOptional`1` runs every check and returns `{ preview }` (what would happen) without changing anything.
Example request
curl -X POST https://post.lnr.ae/api/post/v1/posts/ID/retry \
-H "Authorization: Bearer YOUR_API_KEY"Example response
HTTP 200{
"data": {
"id": "3f8f6c1e-2b4a-4c3d-9e5f-1a2b3c4d5e6f",
"platform": "instagram",
"accountId": "c4a1e2f3-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"status": "scheduled",
"caption": "Cold brew is back. Twelve hours steeped, zero bitterness.",
"hashtags": [
"coldbrew",
"acmecoffee"
],
"scheduledFor": "2026-11-20T05:08:00.000Z",
"publishedAt": null,
"externalUrl": null,
"failureReason": null,
"videoId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"mediaId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"profileId": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
},
"error": null
}/api/post/v1/posts/{id}/approveApprove a post
ApproveApproves a post waiting for review (status `ready`), which schedules it. Needs a key with the approve scope, which only the workspace owner can make.
MCP tool: approve_post
Parameters
idstring (uuid)PathRequiredThe post's id, from Schedule a post (postIds) or List posts.
preview"1"QueryOptional`1` runs every check and returns `{ preview }` (what would happen) without changing anything.
Example request
curl -X POST https://post.lnr.ae/api/post/v1/posts/ID/approve \
-H "Authorization: Bearer YOUR_API_KEY"Example response
HTTP 200{
"data": {
"id": "3f8f6c1e-2b4a-4c3d-9e5f-1a2b3c4d5e6f",
"status": "scheduled",
"scheduledFor": "2026-11-20T05:00:00.000Z"
},
"error": null
}Usage
Where the workspace stands against its Lunar Post plan this month.
/api/post/v1/usageGet usage
ReadThe plan, this month's posts used against its limit (one post per account posted to, counted by UTC calendar month) and live profiles against the plan's profiles. Limits are hard: past them, writes are refused with `plan_limit`. `plan` is `trial`, `starter`, `growth`, `scale` or `none` (the trial ended without a paid plan: everything stays readable, but posting, uploads and new profiles are refused until a plan is chosen). `trialEndsAt` is when the 14-day trial ends; `renewsAt` is the paid plan's next renewal, and `endsAt` is set instead when it's cancelled.
MCP tool: get_usage
Example request
curl https://post.lnr.ae/api/post/v1/usage \
-H "Authorization: Bearer YOUR_API_KEY"Example response
HTTP 200{
"data": {
"plan": "trial",
"period": "2026-11-01",
"posts": {
"used": 12,
"limit": 50
},
"profiles": {
"used": 2,
"limit": 3
},
"trialEndsAt": "2026-11-28T10:00:00.000Z",
"renewsAt": null,
"endsAt": null
},
"error": null
}