REST · v1

Surgr API reference

Give your agent a clear contract: the fields it can send, the permissions it needs and the results it can read.

Base URLhttps://surgr.ai/api/v1

Send Authorization: Bearer <key>. Keep the key in your own SURGR_API_KEY environment variable.

Keys are scoped by the permissions you tick for that connection. A missing permission returns 403. Reads still require a valid key and apply workspace and platform scope.

On this page

Create a connection

Open Agents after signing in.

  1. Sign in to Surgr, open Agents and choose Connect agent.
  2. Fill Connection name. Under Access, choose Prepare for review (recommended), or tick the exact permissions you intend to grant.
  3. Tick the connected platforms under Allowed accounts. Choose Create connection.
  4. Choose Copy key under Copy this key now. It is shown once; store it securely and expose it to your own terminal as SURGR_API_KEY.
  5. Optionally choose Test read-only connection, then Done. The same key works for REST and MCP.

The default Prepare for review grants draft. Draft and schedule also grants schedule and manage_own; scheduling records the agent’s approval. Choose permissions deliberately.

Start here

Post a video from an agent in 5 calls

Human review is recommended.

The five calls below assume an explicitly trusted Draft and schedule connection (draft, schedule, manage_own), curl, jq and a local demo.mp4. Step 5 records approval by that connection. For human review, keep Prepare for review, set requestReview: true at step 4, and stop there: open the Inbox, check every platform preview and choose Approve & schedule. That web action already schedules the post. Do not run step 5 with a review-only key or treat requestReview as approval.

1Presign the video

Choose your local video and exact byte count. This response reserves media in processing state; the upload URL expires in 300 seconds.

Shell
VIDEO_BYTES=$(wc -c < ./demo.mp4 | tr -d ' ')
curl --fail-with-body --silent --show-error -X POST https://surgr.ai/api/v1/media/presign \
  -H "Authorization: Bearer $SURGR_API_KEY" \
  -H "Content-Type: application/json" \
  --data "{\"fileName\":\"demo.mp4\",\"fileType\":\"video/mp4\",\"fileSize\":$VIDEO_BYTES}" \
  -o presign.json

2Upload to the presigned URL

PUT the same file and MIME type directly to uploadUrl. Do not send your Surgr bearer key to the upload host. Stop if the upload fails.

Shell
UPLOAD_URL=$(jq -r '.uploadUrl' presign.json)
curl --fail-with-body --silent --show-error -X PUT "$UPLOAD_URL" \
  -H "Content-Type: video/mp4" \
  --upload-file ./demo.mp4

3Confirm the media

Confirm media.id after the PUT succeeds. The handler verifies stored size and MIME type before marking it ready.

Shell
MEDIA_ID=$(jq -r '.media.id' presign.json)
curl --fail-with-body --silent --show-error -X POST "https://surgr.ai/api/v1/media/$MEDIA_ID/confirm" \
  -H "Authorization: Bearer $SURGR_API_KEY" \
  -o media.json

4Create the video post

Use the ready media ID, targetPlatforms and platformText. Replace the example future time; connect and allow both X and YouTube. For the recommended human-review flow, set requestReview to true and stop after this call.

Shell
SCHEDULED_AT='2030-01-02T09:00:00Z' # Replace with a real future time.
jq -n --arg media "$(jq -r '.id' media.json)" --arg time "$SCHEDULED_AT" '{
  title: "A small product update", targetPlatforms: ["x_twitter", "youtube"],
  contentFormat: "video", proposedAt: $time, requestReview: false,
  threadParts: [{body: "A small product update.", sortOrder: 0, mediaIds: [$media]}],
  platformText: [
    {platform: "x_twitter", caption: "Here is the update in one minute."},
    {platform: "youtube", title: "A small product update", caption: "A quick tour of what changed."}
  ]
}' > post-body.json
curl --fail-with-body --silent --show-error -X POST https://surgr.ai/api/v1/posts \
  -H "Authorization: Bearer $SURGR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: example-video-create-1" \
  --data @post-body.json -o post.json

5Schedule with the granted permission

This trusted-connection call approves its own draft and queues eligible targets. A quota-locked response stays draft; an HTTP 200 is not a published receipt. Read the post again to inspect delivery.

Shell
POST_ID=$(jq -r '.id' post.json)
jq -n --arg time "$SCHEDULED_AT" --argjson revision "$(jq '.revision' post.json)" '{
  scheduledAt: $time, expectedRevision: $revision, requestId: "example-video-schedule-1"
}' > schedule-body.json
curl --fail-with-body --silent --show-error -X POST "https://surgr.ai/api/v1/posts/$POST_ID/schedule" \
  -H "Authorization: Bearer $SURGR_API_KEY" \
  -H "Content-Type: application/json" \
  --data @schedule-body.json

Responses and errors

Examples use invented IDs, reserved example.invalid URLs and illustrative values. Replace them before use. An empty list returns []; there is no example data from a real account.

scheduled means queued, not published. A quota-locked schedule may return HTTP 200 with status: draft and autopublishLocked: true. Check the post’s targets for delivery, errors and platform URLs.

Shared failures return 401 for authentication or 429 for configured rate limiting. JSON errors may include error, code, details or currentRevision. Rate limits use IP-based categories and can include Retry-After and X-RateLimit-* headers; development bypasses them.

For an agent-readable copy, use the full Markdown reference or OpenAPI JSON.

Account

GET/api/v1/account

Read account, workspace, connected accounts and this key’s effective scope.

PermissionNo action permission; valid scoped key required for reads.

Fields, examples & errors
  • Connections are restricted to allowed platforms. X uses the response key x, while platform enums use x_twitter. Subscription plan is workspace-derived.

Request

Shell
curl --fail-with-body --silent --show-error -X GET "https://surgr.ai/api/v1/account" \
  -H "Authorization: Bearer $SURGR_API_KEY"

Success example · HTTP 200

JSON
{
  "id": "user_example",
  "name": "Example founder",
  "email": "[email protected]",
  "image": null,
  "timezone": "Asia/Taipei",
  "createdAt": "2030-01-01T00:00:00.000Z",
  "subscriptionStatus": null,
  "plan": "free",
  "workspace": {
    "id": "workspace_example"
  },
  "connections": {
    "x": {
      "username": null,
      "connectedAt": null
    }
  },
  "agentConnection": {
    "name": "Example agent",
    "accessMode": "prepare_review",
    "permissions": [
      "draft"
    ],
    "allowedPlatforms": [
      "x_twitter"
    ]
  }
}

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.
404
Account or workspace not found.

Posts

GET/api/v1/backlog

Read all scoped drafts, newest first; no pagination or query filters.

PermissionNo action permission; valid scoped key required for reads.

Fields, examples & errors
  • Returns an array of post records, with threadParts, selected target delivery fields, and _count.threadParts/_count.targets. An empty backlog returns [].

Request

Shell
curl --fail-with-body --silent --show-error -X GET "https://surgr.ai/api/v1/backlog" \
  -H "Authorization: Bearer $SURGR_API_KEY"

Success example · HTTP 200

JSON
[]

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.

GET/api/v1/posts

List scoped posts with filters, ordering and pagination.

PermissionNo action permission; valid scoped key required for reads.

Fields, examples & errors

Parameters

FieldTypeRequiredDetails
status"draft" | "scheduled" | "publishing" | "published" | "failed"NoFilter by draft, scheduled, publishing, published or failed.
platform"x_twitter" | "instagram" | "tiktok" | "youtube" | "linkedin" | "pinterest"NoExact platform enum; filters the primary post.platform field, not all targets.
searchstringNoAt most 200 characters; case-insensitive title or thread-part search.
tagsstringNoComma-separated tags; matches any tag after trimming empty entries.
pageintegerNoInteger ≥1; default 1.
limitintegerNoInteger 1–100; default 20.
sortBy"createdAt" | "updatedAt" | "scheduledAt" | "sortOrder"NocreatedAt, updatedAt, scheduledAt or sortOrder; default createdAt.
sortOrder"asc" | "desc"Noasc or desc; default desc.
scheduledAtGtestringNoAccepted and validated ISO datetime, but the current list handler does not apply this filter. Use timeline from/to for a date range.
scheduledAtLtestringNoAccepted and validated ISO datetime, but the current list handler does not apply this filter. Use timeline from/to for a date range.
statusOptional
"draft" | "scheduled" | "publishing" | "published" | "failed"

Filter by draft, scheduled, publishing, published or failed.

platformOptional
"x_twitter" | "instagram" | "tiktok" | "youtube" | "linkedin" | "pinterest"

Exact platform enum; filters the primary post.platform field, not all targets.

searchOptional
string

At most 200 characters; case-insensitive title or thread-part search.

tagsOptional
string

Comma-separated tags; matches any tag after trimming empty entries.

pageOptional
integer

Integer ≥1; default 1.

limitOptional
integer

Integer 1–100; default 20.

sortByOptional
"createdAt" | "updatedAt" | "scheduledAt" | "sortOrder"

createdAt, updatedAt, scheduledAt or sortOrder; default createdAt.

sortOrderOptional
"asc" | "desc"

asc or desc; default desc.

scheduledAtGteOptional
string

Accepted and validated ISO datetime, but the current list handler does not apply this filter. Use timeline from/to for a date range.

scheduledAtLteOptional
string

Accepted and validated ISO datetime, but the current list handler does not apply this filter. Use timeline from/to for a date range.

  • scheduledAtGte and scheduledAtLte are validated but currently ignored by this handler. Use /timeline from/to instead.

Request

Shell
curl --fail-with-body --silent --show-error -X GET "https://surgr.ai/api/v1/posts" \
  -H "Authorization: Bearer $SURGR_API_KEY"

Success example · HTTP 200

JSON
{
  "data": [
    {
      "id": "post_example",
      "userId": "user_example",
      "workspaceId": "workspace_example",
      "createdAt": "2030-01-01T00:00:00.000Z",
      "updatedAt": "2030-01-01T00:00:00.000Z",
      "title": "Launch update",
      "platform": "x_twitter",
      "targetPlatforms": [
        "x_twitter"
      ],
      "contentFormat": "text",
      "status": "draft",
      "revision": 1,
      "reviewState": "needs_review",
      "reviewEnforcedAt": "2030-01-01T00:00:00.000Z",
      "reviewRequestedAt": "2030-01-01T00:00:00.000Z",
      "proposedAt": null,
      "approvedRevision": null,
      "approvedAt": null,
      "approvedByUserId": null,
      "approvedByApiKeyId": null,
      "approvedPlatforms": [],
      "approvedScheduledAt": null,
      "scheduleRevision": 0,
      "createdByApiKeyId": "connection_example",
      "createdByName": "Example agent",
      "clientRequestId": null,
      "scheduledAt": null,
      "publishedAt": null,
      "platformPostId": null,
      "platformUrl": null,
      "qstashMessageId": null,
      "autopublishEnabled": false,
      "lastError": null,
      "retryCount": 0,
      "maxRetries": 3,
      "publishingLockId": null,
      "publishingLockedAt": null,
      "tags": [
        "launch"
      ],
      "source": "api",
      "linkUrl": null,
      "campaign": null,
      "campaignContent": null,
      "notes": null,
      "sortOrder": 0,
      "threadParts": [
        {
          "id": "part_example",
          "postId": "post_example",
          "body": "A small launch update.",
          "sortOrder": 0,
          "createdAt": "2030-01-01T00:00:00.000Z",
          "updatedAt": "2030-01-01T00:00:00.000Z",
          "platformPartId": null,
          "media": []
        }
      ],
      "targets": [
        {
          "id": "target_example",
          "postId": "post_example",
          "platform": "x_twitter",
          "mode": "x_text",
          "requiresManual": false,
          "unsupportedReason": null,
          "metadata": null,
          "caption": "A small launch update.",
          "title": null,
          "status": "draft",
          "createdAt": "2030-01-01T00:00:00.000Z",
          "updatedAt": "2030-01-01T00:00:00.000Z",
          "scheduledAt": null,
          "publishedAt": null,
          "platformPostId": null,
          "platformUrl": null,
          "qstashMessageId": null,
          "autopublishEnabled": false,
          "lastError": null,
          "retryCount": 0,
          "maxRetries": 3,
          "publishingLockId": null,
          "publishingLockedAt": null
        }
      ],
      "_count": {
        "threadParts": 1
      }
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 20,
  "totalPages": 1
}

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.
400
Query validation failed.

POST/api/v1/posts

Create a draft with thread parts, media and per-platform text.

Permissiondraft

Fields, examples & errors

JSON body

FieldTypeRequiredDetails
titlestringNoShared title: at most 280 characters. Update accepts null to clear it.
platform"x_twitter" | "instagram" | "tiktok" | "youtube" | "linkedin" | "pinterest"NoPrimary platform enum; create defaults to x_twitter. The first normalized target becomes primary.
targetPlatformsArray<"x_twitter" | "instagram" | "tiktok" | "youtube" | "linkedin" | "pinterest">NoUp to 6 platform enum values. Empty or omitted falls back to platform; duplicates are normalized.
contentFormat"text" | "thread" | "single_image" | "carousel" | "video" | "document" | "mixed"NoExact accepted format enum; create defaults to text. Media can cause the handler to infer a different format.
threadPartsobject[]YesAt least 1 object. Each has body (1–4000 chars), sortOrder (integer ≥0) and optional mediaIds (up to 10 owned media IDs). On PATCH, missing or empty mediaIds keeps existing media for that sortOrder; it does not detach it.
tagsstring[]NoUp to 10 strings, each at most 50 characters.
source"web" | "api" | "claude" | "openclaw" | "generated"NoAccepted source enum, default web in validation; this API handler always stores api.
notesstringNoAt most 5000 characters. Update accepts null to clear it.
proposedAtstringNoISO 8601 datetime with timezone offset. Proposes a time; does not schedule. PATCH also accepts null.
requestReviewbooleanNoTrue sends the draft to the Inbox. Create defaults false; does not approve it.
linkUrlstring | nullNoAbsolute URL, trimmed, at most 2048 characters; null clears it. {link} in text is resolved by applicable workspace link rules.
campaignstring | nullNoTrimmed and lowercased; pattern ^[a-z0-9][a-z0-9_-]{0,79}$. Null clears it.
campaignContentstring | nullNoSame campaign-tag pattern; identifies one piece of content. Null clears it.
platformTextobject[]NoUp to 6 unique platform entries: platform (exact enum), optional caption (≤4000 chars, nullable), optional title (≤280 chars, nullable). Null or empty caption/title clears that field’s override and uses shared text. Top-level platformText must be an array, not null. On PATCH, omitted or [] preserves existing overrides. Only selected targets receive the overrides.
titleOptional
string

Shared title: at most 280 characters. Update accepts null to clear it.

platformOptional
"x_twitter" | "instagram" | "tiktok" | "youtube" | "linkedin" | "pinterest"

Primary platform enum; create defaults to x_twitter. The first normalized target becomes primary.

targetPlatformsOptional
Array<"x_twitter" | "instagram" | "tiktok" | "youtube" | "linkedin" | "pinterest">

Up to 6 platform enum values. Empty or omitted falls back to platform; duplicates are normalized.

contentFormatOptional
"text" | "thread" | "single_image" | "carousel" | "video" | "document" | "mixed"

Exact accepted format enum; create defaults to text. Media can cause the handler to infer a different format.

threadPartsRequired
object[]

At least 1 object. Each has body (1–4000 chars), sortOrder (integer ≥0) and optional mediaIds (up to 10 owned media IDs). On PATCH, missing or empty mediaIds keeps existing media for that sortOrder; it does not detach it.

tagsOptional
string[]

Up to 10 strings, each at most 50 characters.

sourceOptional
"web" | "api" | "claude" | "openclaw" | "generated"

Accepted source enum, default web in validation; this API handler always stores api.

notesOptional
string

At most 5000 characters. Update accepts null to clear it.

proposedAtOptional
string

ISO 8601 datetime with timezone offset. Proposes a time; does not schedule. PATCH also accepts null.

requestReviewOptional
boolean

True sends the draft to the Inbox. Create defaults false; does not approve it.

linkUrlOptional
string | null

Absolute URL, trimmed, at most 2048 characters; null clears it. {link} in text is resolved by applicable workspace link rules.

campaignOptional
string | null

Trimmed and lowercased; pattern ^[a-z0-9][a-z0-9_-]{0,79}$. Null clears it.

campaignContentOptional
string | null

Same campaign-tag pattern; identifies one piece of content. Null clears it.

platformTextOptional
object[]

Up to 6 unique platform entries: platform (exact enum), optional caption (≤4000 chars, nullable), optional title (≤280 chars, nullable). Null or empty caption/title clears that field’s override and uses shared text. Top-level platformText must be an array, not null. On PATCH, omitted or [] preserves existing overrides. Only selected targets receive the overrides.

  • Idempotency-Key replays the earlier draft with HTTP 200 and Idempotency-Replayed: true. This handler does not compare the new payload with the original; never reuse a key for new content.
  • The response is the post record, including targets and threadParts with media joins. source is always api.

Request

Shell
curl --fail-with-body --silent --show-error -X POST "https://surgr.ai/api/v1/posts" \
  -H "Authorization: Bearer $SURGR_API_KEY" \
  -H "Idempotency-Key: example-create-1" \
  -H "Content-Type: application/json" \
  --data '{"title":"Launch update","targetPlatforms":["x_twitter"],"contentFormat":"text","threadParts":[{"body":"A small launch update.","sortOrder":0}],"tags":["launch"],"requestReview":true,"platformText":[{"platform":"x_twitter","caption":"A small launch update."}]}'

Success example · HTTP 201

JSON
{
  "id": "post_example",
  "userId": "user_example",
  "workspaceId": "workspace_example",
  "createdAt": "2030-01-01T00:00:00.000Z",
  "updatedAt": "2030-01-01T00:00:00.000Z",
  "title": "Launch update",
  "platform": "x_twitter",
  "targetPlatforms": [
    "x_twitter"
  ],
  "contentFormat": "text",
  "status": "draft",
  "revision": 1,
  "reviewState": "needs_review",
  "reviewEnforcedAt": "2030-01-01T00:00:00.000Z",
  "reviewRequestedAt": "2030-01-01T00:00:00.000Z",
  "proposedAt": null,
  "approvedRevision": null,
  "approvedAt": null,
  "approvedByUserId": null,
  "approvedByApiKeyId": null,
  "approvedPlatforms": [],
  "approvedScheduledAt": null,
  "scheduleRevision": 0,
  "createdByApiKeyId": "connection_example",
  "createdByName": "Example agent",
  "clientRequestId": null,
  "scheduledAt": null,
  "publishedAt": null,
  "platformPostId": null,
  "platformUrl": null,
  "qstashMessageId": null,
  "autopublishEnabled": false,
  "lastError": null,
  "retryCount": 0,
  "maxRetries": 3,
  "publishingLockId": null,
  "publishingLockedAt": null,
  "tags": [
    "launch"
  ],
  "source": "api",
  "linkUrl": null,
  "campaign": null,
  "campaignContent": null,
  "notes": null,
  "sortOrder": 0,
  "threadParts": [
    {
      "id": "part_example",
      "postId": "post_example",
      "body": "A small launch update.",
      "sortOrder": 0,
      "createdAt": "2030-01-01T00:00:00.000Z",
      "updatedAt": "2030-01-01T00:00:00.000Z",
      "platformPartId": null,
      "media": []
    }
  ],
  "targets": [
    {
      "id": "target_example",
      "postId": "post_example",
      "platform": "x_twitter",
      "mode": "x_text",
      "requiresManual": false,
      "unsupportedReason": null,
      "metadata": null,
      "caption": "A small launch update.",
      "title": null,
      "status": "draft",
      "createdAt": "2030-01-01T00:00:00.000Z",
      "updatedAt": "2030-01-01T00:00:00.000Z",
      "scheduledAt": null,
      "publishedAt": null,
      "platformPostId": null,
      "platformUrl": null,
      "qstashMessageId": null,
      "autopublishEnabled": false,
      "lastError": null,
      "retryCount": 0,
      "maxRetries": 3,
      "publishingLockId": null,
      "publishingLockedAt": null
    }
  ]
}

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.
400
Body validation failed.
403
Missing draft permission, disallowed platform or media outside this workspace.

POST/api/v1/posts/bulk

Create 1–50 drafts in one transaction.

Permissiondraft

Fields, examples & errors

JSON body

FieldTypeRequiredDetails
postsobject[]Yes1–50 create-post objects; nested fields and validation match POST /api/v1/posts. The batch is transactional.
postsRequired
object[]

1–50 create-post objects; nested fields and validation match POST /api/v1/posts. The batch is transactional.

  • Response posts include threadParts/media joins; this handler does not include a targets relation in its hydrated response.

Request

Shell
curl --fail-with-body --silent --show-error -X POST "https://surgr.ai/api/v1/posts/bulk" \
  -H "Authorization: Bearer $SURGR_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"posts":[{"title":"Launch update","targetPlatforms":["x_twitter"],"contentFormat":"text","threadParts":[{"body":"A small launch update.","sortOrder":0}],"tags":["launch"],"requestReview":true,"platformText":[{"platform":"x_twitter","caption":"A small launch update."}]}]}'

Success example · HTTP 201

JSON
{
  "created": 1,
  "posts": [
    {
      "id": "post_example",
      "userId": "user_example",
      "workspaceId": "workspace_example",
      "createdAt": "2030-01-01T00:00:00.000Z",
      "updatedAt": "2030-01-01T00:00:00.000Z",
      "title": "Launch update",
      "platform": "x_twitter",
      "targetPlatforms": [
        "x_twitter"
      ],
      "contentFormat": "text",
      "status": "draft",
      "revision": 1,
      "reviewState": "needs_review",
      "reviewEnforcedAt": "2030-01-01T00:00:00.000Z",
      "reviewRequestedAt": "2030-01-01T00:00:00.000Z",
      "proposedAt": null,
      "approvedRevision": null,
      "approvedAt": null,
      "approvedByUserId": null,
      "approvedByApiKeyId": null,
      "approvedPlatforms": [],
      "approvedScheduledAt": null,
      "scheduleRevision": 0,
      "createdByApiKeyId": "connection_example",
      "createdByName": "Example agent",
      "clientRequestId": null,
      "scheduledAt": null,
      "publishedAt": null,
      "platformPostId": null,
      "platformUrl": null,
      "qstashMessageId": null,
      "autopublishEnabled": false,
      "lastError": null,
      "retryCount": 0,
      "maxRetries": 3,
      "publishingLockId": null,
      "publishingLockedAt": null,
      "tags": [
        "launch"
      ],
      "source": "api",
      "linkUrl": null,
      "campaign": null,
      "campaignContent": null,
      "notes": null,
      "sortOrder": 0,
      "threadParts": [
        {
          "id": "part_example",
          "postId": "post_example",
          "body": "A small launch update.",
          "sortOrder": 0,
          "createdAt": "2030-01-01T00:00:00.000Z",
          "updatedAt": "2030-01-01T00:00:00.000Z",
          "platformPartId": null,
          "media": []
        }
      ]
    }
  ]
}

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.
400
Invalid batch or create-post body.
403
Missing draft permission, disallowed platform or foreign media.

GET/api/v1/posts/{id}

Read a scoped post, its targets, feedback and recent activity.

PermissionNo action permission; valid scoped key required for reads.

Fields, examples & errors

Parameters

FieldTypeRequiredDetails
idstringYesID returned by the relevant create/presign response; fake example IDs must be replaced.
idRequired
string

ID returned by the relevant create/presign response; fake example IDs must be replaced.

  • Includes threadParts/media, targets, the latest 5 publishLogs, all changeRequests and latest 20 integrationActivities.

Request

Shell
curl --fail-with-body --silent --show-error -X GET "https://surgr.ai/api/v1/posts/post_example" \
  -H "Authorization: Bearer $SURGR_API_KEY"

Success example · HTTP 200

JSON
{
  "id": "post_example",
  "userId": "user_example",
  "workspaceId": "workspace_example",
  "createdAt": "2030-01-01T00:00:00.000Z",
  "updatedAt": "2030-01-01T00:00:00.000Z",
  "title": "Launch update",
  "platform": "x_twitter",
  "targetPlatforms": [
    "x_twitter"
  ],
  "contentFormat": "text",
  "status": "draft",
  "revision": 1,
  "reviewState": "needs_review",
  "reviewEnforcedAt": "2030-01-01T00:00:00.000Z",
  "reviewRequestedAt": "2030-01-01T00:00:00.000Z",
  "proposedAt": null,
  "approvedRevision": null,
  "approvedAt": null,
  "approvedByUserId": null,
  "approvedByApiKeyId": null,
  "approvedPlatforms": [],
  "approvedScheduledAt": null,
  "scheduleRevision": 0,
  "createdByApiKeyId": "connection_example",
  "createdByName": "Example agent",
  "clientRequestId": null,
  "scheduledAt": null,
  "publishedAt": null,
  "platformPostId": null,
  "platformUrl": null,
  "qstashMessageId": null,
  "autopublishEnabled": false,
  "lastError": null,
  "retryCount": 0,
  "maxRetries": 3,
  "publishingLockId": null,
  "publishingLockedAt": null,
  "tags": [
    "launch"
  ],
  "source": "api",
  "linkUrl": null,
  "campaign": null,
  "campaignContent": null,
  "notes": null,
  "sortOrder": 0,
  "threadParts": [
    {
      "id": "part_example",
      "postId": "post_example",
      "body": "A small launch update.",
      "sortOrder": 0,
      "createdAt": "2030-01-01T00:00:00.000Z",
      "updatedAt": "2030-01-01T00:00:00.000Z",
      "platformPartId": null,
      "media": []
    }
  ],
  "targets": [
    {
      "id": "target_example",
      "postId": "post_example",
      "platform": "x_twitter",
      "mode": "x_text",
      "requiresManual": false,
      "unsupportedReason": null,
      "metadata": null,
      "caption": "A small launch update.",
      "title": null,
      "status": "draft",
      "createdAt": "2030-01-01T00:00:00.000Z",
      "updatedAt": "2030-01-01T00:00:00.000Z",
      "scheduledAt": null,
      "publishedAt": null,
      "platformPostId": null,
      "platformUrl": null,
      "qstashMessageId": null,
      "autopublishEnabled": false,
      "lastError": null,
      "retryCount": 0,
      "maxRetries": 3,
      "publishingLockId": null,
      "publishingLockedAt": null
    }
  ],
  "publishLogs": [],
  "changeRequests": [],
  "integrationActivities": []
}

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.
400
Post ID missing.
403
Post outside the workspace or allowed platform scope.
404
Post not found.

PATCH/api/v1/posts/{id}

Edit a scoped mutable post; material edits clear its approval and schedule.

Permissiondraft; also manage_own for self-approved posts or manage_approved for posts approved by a person/another connection

Fields, examples & errors

Parameters

FieldTypeRequiredDetails
idstringYesID returned by the relevant create/presign response; fake example IDs must be replaced.
idRequired
string

ID returned by the relevant create/presign response; fake example IDs must be replaced.

JSON body

FieldTypeRequiredDetails
titlestring | nullNoShared title: at most 280 characters. Update accepts null to clear it.
platform"x_twitter" | "instagram" | "tiktok" | "youtube" | "linkedin" | "pinterest"NoPrimary platform enum; create defaults to x_twitter. The first normalized target becomes primary.
targetPlatformsArray<"x_twitter" | "instagram" | "tiktok" | "youtube" | "linkedin" | "pinterest">NoUp to 6 platform enum values. Empty or omitted falls back to platform; duplicates are normalized.
contentFormat"text" | "thread" | "single_image" | "carousel" | "video" | "document" | "mixed"NoExact accepted format enum; create defaults to text. Media can cause the handler to infer a different format.
tagsstring[]NoUp to 10 strings, each at most 50 characters.
threadPartsobject[]NoAt least 1 object. Each has body (1–4000 chars), sortOrder (integer ≥0) and optional mediaIds (up to 10 owned media IDs). On PATCH, missing or empty mediaIds keeps existing media for that sortOrder; it does not detach it.
notesstring | nullNoAt most 5000 characters. Update accepts null to clear it.
scheduledAtstring | nullNoISO 8601 datetime with timezone offset. Schedule endpoint requires a future time; PATCH accepts it only for legacy_full keys (or returns 403).
status"draft" | "scheduled" | "publishing" | "failed"NoPATCH accepts draft, scheduled, publishing or failed only for legacy_full keys. Modern scoped keys must use schedule/unschedule operations.
proposedAtstring | nullNoISO 8601 datetime with timezone offset. Proposes a time; does not schedule. PATCH also accepts null.
requestReviewbooleanNoTrue sends the draft to the Inbox. Create defaults false; does not approve it.
expectedRevisionintegerNoOptional integer ≥1; mismatches return 409 stale_revision. Use the revision returned by the last read.
linkUrlstring | nullNoAbsolute URL, trimmed, at most 2048 characters; null clears it. {link} in text is resolved by applicable workspace link rules.
campaignstring | nullNoTrimmed and lowercased; pattern ^[a-z0-9][a-z0-9_-]{0,79}$. Null clears it.
campaignContentstring | nullNoSame campaign-tag pattern; identifies one piece of content. Null clears it.
platformTextobject[]NoUp to 6 unique platform entries: platform (exact enum), optional caption (≤4000 chars, nullable), optional title (≤280 chars, nullable). Null or empty caption/title clears that field’s override and uses shared text. Top-level platformText must be an array, not null. On PATCH, omitted or [] preserves existing overrides. Only selected targets receive the overrides.
titleOptional
string | null

Shared title: at most 280 characters. Update accepts null to clear it.

platformOptional
"x_twitter" | "instagram" | "tiktok" | "youtube" | "linkedin" | "pinterest"

Primary platform enum; create defaults to x_twitter. The first normalized target becomes primary.

targetPlatformsOptional
Array<"x_twitter" | "instagram" | "tiktok" | "youtube" | "linkedin" | "pinterest">

Up to 6 platform enum values. Empty or omitted falls back to platform; duplicates are normalized.

contentFormatOptional
"text" | "thread" | "single_image" | "carousel" | "video" | "document" | "mixed"

Exact accepted format enum; create defaults to text. Media can cause the handler to infer a different format.

tagsOptional
string[]

Up to 10 strings, each at most 50 characters.

threadPartsOptional
object[]

At least 1 object. Each has body (1–4000 chars), sortOrder (integer ≥0) and optional mediaIds (up to 10 owned media IDs). On PATCH, missing or empty mediaIds keeps existing media for that sortOrder; it does not detach it.

notesOptional
string | null

At most 5000 characters. Update accepts null to clear it.

scheduledAtOptional
string | null

ISO 8601 datetime with timezone offset. Schedule endpoint requires a future time; PATCH accepts it only for legacy_full keys (or returns 403).

statusOptional
"draft" | "scheduled" | "publishing" | "failed"

PATCH accepts draft, scheduled, publishing or failed only for legacy_full keys. Modern scoped keys must use schedule/unschedule operations.

proposedAtOptional
string | null

ISO 8601 datetime with timezone offset. Proposes a time; does not schedule. PATCH also accepts null.

requestReviewOptional
boolean

True sends the draft to the Inbox. Create defaults false; does not approve it.

expectedRevisionOptional
integer

Optional integer ≥1; mismatches return 409 stale_revision. Use the revision returned by the last read.

linkUrlOptional
string | null

Absolute URL, trimmed, at most 2048 characters; null clears it. {link} in text is resolved by applicable workspace link rules.

campaignOptional
string | null

Trimmed and lowercased; pattern ^[a-z0-9][a-z0-9_-]{0,79}$. Null clears it.

campaignContentOptional
string | null

Same campaign-tag pattern; identifies one piece of content. Null clears it.

platformTextOptional
object[]

Up to 6 unique platform entries: platform (exact enum), optional caption (≤4000 chars, nullable), optional title (≤280 chars, nullable). Null or empty caption/title clears that field’s override and uses shared text. Top-level platformText must be an array, not null. On PATCH, omitted or [] preserves existing overrides. Only selected targets receive the overrides.

  • scheduledAt/status are schema-accepted but forbidden for non-legacy keys; use scheduling operations.
  • An edit of any already-approved post clears approval/schedule. threadParts, platforms, format and link changes also invalidate the revision. PATCH with missing or empty mediaIds preserves the prior media at that sortOrder.

Request

Shell
curl --fail-with-body --silent --show-error -X PATCH "https://surgr.ai/api/v1/posts/post_example" \
  -H "Authorization: Bearer $SURGR_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"title":"Updated launch note","expectedRevision":1}'

Success example · HTTP 200

JSON
{
  "id": "post_example",
  "userId": "user_example",
  "workspaceId": "workspace_example",
  "createdAt": "2030-01-01T00:00:00.000Z",
  "updatedAt": "2030-01-01T00:00:00.000Z",
  "title": "Updated launch note",
  "platform": "x_twitter",
  "targetPlatforms": [
    "x_twitter"
  ],
  "contentFormat": "text",
  "status": "draft",
  "revision": 1,
  "reviewState": "needs_review",
  "reviewEnforcedAt": "2030-01-01T00:00:00.000Z",
  "reviewRequestedAt": "2030-01-01T00:00:00.000Z",
  "proposedAt": null,
  "approvedRevision": null,
  "approvedAt": null,
  "approvedByUserId": null,
  "approvedByApiKeyId": null,
  "approvedPlatforms": [],
  "approvedScheduledAt": null,
  "scheduleRevision": 0,
  "createdByApiKeyId": "connection_example",
  "createdByName": "Example agent",
  "clientRequestId": null,
  "scheduledAt": null,
  "publishedAt": null,
  "platformPostId": null,
  "platformUrl": null,
  "qstashMessageId": null,
  "autopublishEnabled": false,
  "lastError": null,
  "retryCount": 0,
  "maxRetries": 3,
  "publishingLockId": null,
  "publishingLockedAt": null,
  "tags": [
    "launch"
  ],
  "source": "api",
  "linkUrl": null,
  "campaign": null,
  "campaignContent": null,
  "notes": null,
  "sortOrder": 0,
  "threadParts": [
    {
      "id": "part_example",
      "postId": "post_example",
      "body": "A small launch update.",
      "sortOrder": 0,
      "createdAt": "2030-01-01T00:00:00.000Z",
      "updatedAt": "2030-01-01T00:00:00.000Z",
      "platformPartId": null,
      "media": []
    }
  ],
  "targets": [
    {
      "id": "target_example",
      "postId": "post_example",
      "platform": "x_twitter",
      "mode": "x_text",
      "requiresManual": false,
      "unsupportedReason": null,
      "metadata": null,
      "caption": "A small launch update.",
      "title": null,
      "status": "draft",
      "createdAt": "2030-01-01T00:00:00.000Z",
      "updatedAt": "2030-01-01T00:00:00.000Z",
      "scheduledAt": null,
      "publishedAt": null,
      "platformPostId": null,
      "platformUrl": null,
      "qstashMessageId": null,
      "autopublishEnabled": false,
      "lastError": null,
      "retryCount": 0,
      "maxRetries": 3,
      "publishingLockId": null,
      "publishingLockedAt": null
    }
  ]
}

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.
400
Missing ID or body validation failed.
403
Permission/scope refusal, foreign media, or delivery-state fields on a non-legacy key.
404
Post not found.
409
Stale revision, publishing in progress, or published post cannot be changed.

DELETE/api/v1/posts/{id}

Delete a mutable post and clean up media with no remaining references.

Permissiondraft; also manage_own for self-approved posts or manage_approved for posts approved by a person/another connection

Fields, examples & errors

Parameters

FieldTypeRequiredDetails
idstringYesID returned by the relevant create/presign response; fake example IDs must be replaced.
idRequired
string

ID returned by the relevant create/presign response; fake example IDs must be replaced.

  • Irreversible deletion; shared media still attached elsewhere is preserved. Storage cleanup is asynchronous best effort.

Request

Shell
curl --fail-with-body --silent --show-error -X DELETE "https://surgr.ai/api/v1/posts/post_example" \
  -H "Authorization: Bearer $SURGR_API_KEY"

Success example · HTTP 200

JSON
{
  "success": true
}

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.
400
Post ID missing.
403
Permission or workspace/platform scope refusal.
404
Post not found.
409
Publishing in progress or already-published post cannot be deleted.

Scheduling

POST/api/v1/posts/{id}/schedule

Queue eligible targets at a future time, preserving or recording a matching approval.

Permissionschedule for new drafts; manage_own for its approvals; dispatch_approved or manage_approved for matching approvals by others

Fields, examples & errors

Parameters

FieldTypeRequiredDetails
idstringYesID returned by the relevant create/presign response; fake example IDs must be replaced.
idRequired
string

ID returned by the relevant create/presign response; fake example IDs must be replaced.

JSON body

FieldTypeRequiredDetails
scheduledAtstringYesISO 8601 datetime with timezone offset. Schedule endpoint requires a future time; PATCH accepts it only for legacy_full keys (or returns 403).
expectedRevisionintegerNoOptional integer ≥1; mismatches return 409 stale_revision. Use the revision returned by the last read.
requestIdstringNoOptional string, 1–200 characters. Schedule idempotency ID; falls back to the Idempotency-Key header. Do not reuse for another payload.
scheduledAtRequired
string

ISO 8601 datetime with timezone offset. Schedule endpoint requires a future time; PATCH accepts it only for legacy_full keys (or returns 403).

expectedRevisionOptional
integer

Optional integer ≥1; mismatches return 409 stale_revision. Use the revision returned by the last read.

requestIdOptional
string

Optional string, 1–200 characters. Schedule idempotency ID; falls back to the Idempotency-Key header. Do not reuse for another payload.

  • For review-first use, create/request review, then stop for a person to use Inbox Approve & schedule; that action already queues the post. dispatch_approved is only for re-dispatching a still-current exact approval: first reread the post and preserve its approved revision, destinations and time. schedule can self-approve an unapproved draft; do not grant it if every post must get human review.
  • A quota-limited Free account can receive HTTP 200 with status draft, autopublishEnabled false and autopublishLocked true: it was not queued. Normal responses contain targets/threadParts; quota responses include threadParts but not targets.
  • requestId takes precedence over Idempotency-Key. Replays return HTTP 200 / Idempotency-Replayed: true; changed payloads return 409. A request still in progress can return Retry-After: 30.
  • An HTTP 200 scheduled response confirms queueing, not successful publication. Check GET /posts/{id} for each target’s status/lastError/platformUrl.

Request

Shell
curl --fail-with-body --silent --show-error -X POST "https://surgr.ai/api/v1/posts/post_example/schedule" \
  -H "Authorization: Bearer $SURGR_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"scheduledAt":"2030-01-02T09:00:00Z","expectedRevision":1,"requestId":"example-schedule-1"}'

Success example · HTTP 200

JSON
{
  "id": "post_example",
  "userId": "user_example",
  "workspaceId": "workspace_example",
  "createdAt": "2030-01-01T00:00:00.000Z",
  "updatedAt": "2030-01-01T00:00:00.000Z",
  "title": "Launch update",
  "platform": "x_twitter",
  "targetPlatforms": [
    "x_twitter"
  ],
  "contentFormat": "text",
  "status": "scheduled",
  "revision": 1,
  "reviewState": "approved",
  "reviewEnforcedAt": "2030-01-01T00:00:00.000Z",
  "reviewRequestedAt": "2030-01-01T00:00:00.000Z",
  "proposedAt": null,
  "approvedRevision": 1,
  "approvedAt": "2030-01-01T00:00:00.000Z",
  "approvedByUserId": "user_example",
  "approvedByApiKeyId": null,
  "approvedPlatforms": [
    "x_twitter"
  ],
  "approvedScheduledAt": "2030-01-02T09:00:00.000Z",
  "scheduleRevision": 1,
  "createdByApiKeyId": "connection_example",
  "createdByName": "Example agent",
  "clientRequestId": null,
  "scheduledAt": "2030-01-02T09:00:00.000Z",
  "publishedAt": null,
  "platformPostId": null,
  "platformUrl": null,
  "qstashMessageId": "message_example",
  "autopublishEnabled": true,
  "lastError": null,
  "retryCount": 0,
  "maxRetries": 3,
  "publishingLockId": null,
  "publishingLockedAt": null,
  "tags": [
    "launch"
  ],
  "source": "api",
  "linkUrl": null,
  "campaign": null,
  "campaignContent": null,
  "notes": null,
  "sortOrder": 0,
  "threadParts": [
    {
      "id": "part_example",
      "postId": "post_example",
      "body": "A small launch update.",
      "sortOrder": 0,
      "createdAt": "2030-01-01T00:00:00.000Z",
      "updatedAt": "2030-01-01T00:00:00.000Z",
      "platformPartId": null
    }
  ],
  "targets": [
    {
      "id": "target_example",
      "postId": "post_example",
      "platform": "x_twitter",
      "mode": "x_text",
      "requiresManual": false,
      "unsupportedReason": null,
      "metadata": null,
      "caption": "A small launch update.",
      "title": null,
      "status": "scheduled",
      "createdAt": "2030-01-01T00:00:00.000Z",
      "updatedAt": "2030-01-01T00:00:00.000Z",
      "scheduledAt": "2030-01-02T09:00:00.000Z",
      "publishedAt": null,
      "platformPostId": null,
      "platformUrl": null,
      "qstashMessageId": "message_example",
      "autopublishEnabled": true,
      "lastError": null,
      "retryCount": 0,
      "maxRetries": 3,
      "publishingLockId": null,
      "publishingLockedAt": null
    }
  ]
}

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.
400
Missing ID, invalid body, past time, or no thread content.
403
Ownership, platform or permission refusal.
404
Post not found.
409
Publishing paused, no eligible targets, stale/changed approval, changed revision, idempotency conflict or request in progress.
502
Failed to queue publish jobs.

POST/api/v1/posts/{id}/unschedule

Cancel a scheduled post, return it to draft and clear scoped approval.

Permissionmanage_own for self-approved posts; manage_approved for other or missing approvals

Fields, examples & errors

Parameters

FieldTypeRequiredDetails
idstringYesID returned by the relevant create/presign response; fake example IDs must be replaced.
idRequired
string

ID returned by the relevant create/presign response; fake example IDs must be replaced.

  • No request body. The response includes targets and threadParts; scheduleRevision increases.

Request

Shell
curl --fail-with-body --silent --show-error -X POST "https://surgr.ai/api/v1/posts/post_example/unschedule" \
  -H "Authorization: Bearer $SURGR_API_KEY"

Success example · HTTP 200

JSON
{
  "id": "post_example",
  "userId": "user_example",
  "workspaceId": "workspace_example",
  "createdAt": "2030-01-01T00:00:00.000Z",
  "updatedAt": "2030-01-01T00:00:00.000Z",
  "title": "Launch update",
  "platform": "x_twitter",
  "targetPlatforms": [
    "x_twitter"
  ],
  "contentFormat": "text",
  "status": "draft",
  "revision": 1,
  "reviewState": "draft",
  "reviewEnforcedAt": "2030-01-01T00:00:00.000Z",
  "reviewRequestedAt": "2030-01-01T00:00:00.000Z",
  "proposedAt": null,
  "approvedRevision": null,
  "approvedAt": null,
  "approvedByUserId": null,
  "approvedByApiKeyId": null,
  "approvedPlatforms": [],
  "approvedScheduledAt": null,
  "scheduleRevision": 2,
  "createdByApiKeyId": "connection_example",
  "createdByName": "Example agent",
  "clientRequestId": null,
  "scheduledAt": null,
  "publishedAt": null,
  "platformPostId": null,
  "platformUrl": null,
  "qstashMessageId": null,
  "autopublishEnabled": false,
  "lastError": null,
  "retryCount": 0,
  "maxRetries": 3,
  "publishingLockId": null,
  "publishingLockedAt": null,
  "tags": [
    "launch"
  ],
  "source": "api",
  "linkUrl": null,
  "campaign": null,
  "campaignContent": null,
  "notes": null,
  "sortOrder": 0,
  "threadParts": [
    {
      "id": "part_example",
      "postId": "post_example",
      "body": "A small launch update.",
      "sortOrder": 0,
      "createdAt": "2030-01-01T00:00:00.000Z",
      "updatedAt": "2030-01-01T00:00:00.000Z",
      "platformPartId": null
    }
  ],
  "targets": [
    {
      "id": "target_example",
      "postId": "post_example",
      "platform": "x_twitter",
      "mode": "x_text",
      "requiresManual": false,
      "unsupportedReason": null,
      "metadata": null,
      "caption": "A small launch update.",
      "title": null,
      "status": "draft",
      "createdAt": "2030-01-01T00:00:00.000Z",
      "updatedAt": "2030-01-01T00:00:00.000Z",
      "scheduledAt": null,
      "publishedAt": null,
      "platformPostId": null,
      "platformUrl": null,
      "qstashMessageId": null,
      "autopublishEnabled": false,
      "lastError": null,
      "retryCount": 0,
      "maxRetries": 3,
      "publishingLockId": null,
      "publishingLockedAt": null
    }
  ]
}

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.
400
Missing ID or post is not scheduled.
403
Ownership, platform or cancellation permission refusal.
404
Post not found.
409
Post began publishing during cancellation.

POST/api/v1/posts/{id}/publish-now

Enqueue eligible targets immediately; this is not a published receipt.

Permissionpublish_now; also manage_own or manage_approved for an already-approved post

Fields, examples & errors

Parameters

FieldTypeRequiredDetails
idstringYesID returned by the relevant create/presign response; fake example IDs must be replaced.
idRequired
string

ID returned by the relevant create/presign response; fake example IDs must be replaced.

  • Prefer schedule plus human review. No request body. For scoped keys this records the connection’s approval; it does not require prior human review.
  • Response status scheduled means queued. Re-read the post/targets for published or failed delivery.

Request

Shell
curl --fail-with-body --silent --show-error -X POST "https://surgr.ai/api/v1/posts/post_example/publish-now" \
  -H "Authorization: Bearer $SURGR_API_KEY"

Success example · HTTP 200

JSON
{
  "id": "post_example",
  "status": "scheduled",
  "targetCount": 1,
  "message": "Publishing in progress \u2014 this may take a moment for posts with video."
}

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.
400
Missing ID or no thread content.
403
Ownership, platform or permission refusal.
404
Post not found.
409
No eligible targets, publishing in progress or already-published post.

Media

POST/api/v1/media/presign

Reserve media and obtain a 5-minute direct-upload URL.

Permissiondraft

Fields, examples & errors

JSON body

FieldTypeRequiredDetails
fileNamestringYesFilename, 1–255 characters. Case differs from URL-upload filename.
fileTypestringYesMIME type, 1–127 characters; allowed: image/jpeg, image/png, image/gif, image/webp, video/mp4, video/quicktime.
fileSizeintegerYesPositive integer byte count, up to 104857600 (100 MiB). Must match the uploaded object.
fileNameRequired
string

Filename, 1–255 characters. Case differs from URL-upload filename.

fileTypeRequired
string

MIME type, 1–127 characters; allowed: image/jpeg, image/png, image/gif, image/webp, video/mp4, video/quicktime.

fileSizeRequired
integer

Positive integer byte count, up to 104857600 (100 MiB). Must match the uploaded object.

  • PUT the exact byte count and Content-Type to uploadUrl, then confirm media.id. Do not send the Surgr bearer key to the presigned upload host.

Request

Shell
curl --fail-with-body --silent --show-error -X POST "https://surgr.ai/api/v1/media/presign" \
  -H "Authorization: Bearer $SURGR_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"fileName":"demo.mp4","fileType":"video/mp4","fileSize":1000000}'

Success example · HTTP 201

JSON
{
  "uploadUrl": "https://upload.example.invalid/demo.mp4?example=presigned",
  "media": {
    "id": "media_example",
    "userId": "user_example",
    "workspaceId": "workspace_example",
    "createdAt": "2030-01-01T00:00:00.000Z",
    "updatedAt": "2030-01-01T00:00:00.000Z",
    "filename": "demo.mp4",
    "mimeType": "video/mp4",
    "sizeBytes": 1000000,
    "width": null,
    "height": null,
    "durationMs": null,
    "storageUrl": "",
    "storageKey": "media/user_example/demo.mp4",
    "processingStatus": "processing"
  }
}

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.
400
Invalid fields, unsupported MIME or file larger than 104857600 bytes.
403
Missing draft permission.

POST/api/v1/media/upload

Fetch a public URL server-side and create ready media.

Permissiondraft

Fields, examples & errors

JSON body

FieldTypeRequiredDetails
urlstringYesPublic absolute URL; the server downloads it. Private/local destinations and unsafe redirects are refused; download limit 10485760 bytes (10 MiB).
filenamestringNoOptional filename override, at most 255 characters; otherwise derived from URL path.
mimeTypestringNoOptional MIME override, at most 127 characters; otherwise downloaded content type (or application/octet-stream).
urlRequired
string

Public absolute URL; the server downloads it. Private/local destinations and unsafe redirects are refused; download limit 10485760 bytes (10 MiB).

filenameOptional
string

Optional filename override, at most 255 characters; otherwise derived from URL path.

mimeTypeOptional
string

Optional MIME override, at most 127 characters; otherwise downloaded content type (or application/octet-stream).

  • Uses different field casing from presign: filename/mimeType. No confirm call needed; processingStatus is ready. Public media URLs must be replaceable fake examples here.

Request

Shell
curl --fail-with-body --silent --show-error -X POST "https://surgr.ai/api/v1/media/upload" \
  -H "Authorization: Bearer $SURGR_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"url":"https://assets.example.invalid/demo.mp4","filename":"demo.mp4","mimeType":"video/mp4"}'

Success example · HTTP 201

JSON
{
  "id": "media_example",
  "userId": "user_example",
  "workspaceId": "workspace_example",
  "createdAt": "2030-01-01T00:00:00.000Z",
  "updatedAt": "2030-01-01T00:00:00.000Z",
  "filename": "demo.mp4",
  "mimeType": "video/mp4",
  "sizeBytes": 1000000,
  "width": null,
  "height": null,
  "durationMs": null,
  "storageUrl": "https://assets.example.invalid/demo.mp4",
  "storageKey": "media/user_example/demo.mp4",
  "processingStatus": "ready"
}

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.
400
Invalid body, unsafe/private URL or failed/oversized download.
403
Missing draft permission.

POST/api/v1/media/{id}/confirm

Verify a presigned object and mark reserved media ready.

Permissiondraft

Fields, examples & errors

Parameters

FieldTypeRequiredDetails
idstringYesID returned by the relevant create/presign response; fake example IDs must be replaced.
idRequired
string

ID returned by the relevant create/presign response; fake example IDs must be replaced.

  • No request body. Confirm the ID returned by presign after the PUT completes; not idempotent once processingStatus is ready.

Request

Shell
curl --fail-with-body --silent --show-error -X POST "https://surgr.ai/api/v1/media/media_example/confirm" \
  -H "Authorization: Bearer $SURGR_API_KEY"

Success example · HTTP 200

JSON
{
  "id": "media_example",
  "userId": "user_example",
  "workspaceId": "workspace_example",
  "createdAt": "2030-01-01T00:00:00.000Z",
  "updatedAt": "2030-01-01T00:00:00.000Z",
  "filename": "demo.mp4",
  "mimeType": "video/mp4",
  "sizeBytes": 1000000,
  "width": null,
  "height": null,
  "durationMs": null,
  "storageUrl": "https://assets.example.invalid/demo.mp4",
  "storageKey": "media/user_example/demo.mp4",
  "processingStatus": "ready"
}

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.
400
Missing ID or stored upload fails size/content-type verification.
403
Missing draft permission or foreign workspace media.
404
Media not found.
409
Media is not in processing state.

Results

GET/api/v1/stats

Read scoped queue counts, next schedule, seven-day gaps and up to five failed/stuck posts.

PermissionNo action permission; valid scoped key required for reads.

Fields, examples & errors
  • No query fields. Failed posts include id, preview, lastError, failedAt and targetErrors (platform, status, message, failedAt). Stale publishing/scheduled targets use a 10-minute threshold.

Request

Shell
curl --fail-with-body --silent --show-error -X GET "https://surgr.ai/api/v1/stats" \
  -H "Authorization: Bearer $SURGR_API_KEY"

Success example · HTTP 200

JSON
{
  "totalPosts": 0,
  "published": 0,
  "failed": 0,
  "scheduled": 0,
  "drafts": 0,
  "successRate": 100,
  "nextScheduledAt": null,
  "nextScheduledPreview": null,
  "gapDays": 7,
  "scheduledByDay": [
    {
      "date": "2030-01-01",
      "count": 0
    },
    {
      "date": "2030-01-02",
      "count": 0
    },
    {
      "date": "2030-01-03",
      "count": 0
    },
    {
      "date": "2030-01-04",
      "count": 0
    },
    {
      "date": "2030-01-05",
      "count": 0
    },
    {
      "date": "2030-01-06",
      "count": 0
    },
    {
      "date": "2030-01-07",
      "count": 0
    }
  ],
  "failedPosts": []
}

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.

GET/api/v1/results

Read scoped 30-day growth, post metrics, lessons and metric sync status.

PermissionNo action permission; valid scoped key required for reads.

Fields, examples & errors
  • No query fields. summary, growth, daily, followers, topPosts, lessons and syncStatus are returned. Null/stale metrics must not be turned into invented results.
  • daily contains 30 daily points. topPosts/bestPost include per-destination counts; syncStatus reports lastAttemptAt, lastSuccessAt and lastError for connected platforms.

Request

Shell
curl --fail-with-body --silent --show-error -X GET "https://surgr.ai/api/v1/results" \
  -H "Authorization: Bearer $SURGR_API_KEY"

Success example · HTTP 200

JSON
{
  "summary": {
    "followersCurrent": null,
    "followersDelta7d": null,
    "followersDelta30d": null,
    "impressions7d": 0,
    "impressions30d": 0,
    "postsPublished7d": 0,
    "postsPublished30d": 0,
    "engagement30d": 0,
    "averageImpressionsPerPost30d": 0,
    "bestPost30d": null,
    "lastMetricsSyncAt": null,
    "xConnected": false
  },
  "growth": {
    "audienceCurrent": null,
    "audienceDelta7d": null,
    "audienceDeltaPrevious7d": null,
    "followersCurrent": null,
    "followersDelta7d": null,
    "followersDeltaPrevious7d": null,
    "exposure7d": 0,
    "exposurePrevious7d": 0,
    "impressions7d": 0,
    "impressionsPrevious7d": 0,
    "postsPublished7d": 0,
    "postsPublishedPrevious7d": 0,
    "engagement7d": 0,
    "engagementPrevious7d": 0,
    "lastMetricsSyncAt": null,
    "bestPost7d": null,
    "platformBreakdown": [],
    "nextAction": {
      "label": "Connect a platform to collect results.",
      "href": "/settings",
      "tone": "warning"
    },
    "xConnected": false,
    "connectedPlatforms": []
  },
  "daily": [
    {
      "date": "2030-01-02",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-03",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-04",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-05",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-06",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-07",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-08",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-09",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-10",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-11",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-12",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-13",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-14",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-15",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-16",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-17",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-18",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-19",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-20",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-21",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-22",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-23",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-24",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-25",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-26",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-27",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-28",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-29",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-30",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    },
    {
      "date": "2030-01-31",
      "postsPublished": 0,
      "impressionCount": 0,
      "engagementCount": 0
    }
  ],
  "followers": [],
  "topPosts": [],
  "lessons": [
    {
      "title": "Connect a platform",
      "detail": "Connect a social platform to start collecting result snapshots.",
      "tone": "warning"
    }
  ],
  "syncStatus": []
}

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.

GET/api/v1/tags

Read distinct tags and scoped post counts, most frequent first.

PermissionNo action permission; valid scoped key required for reads.

Fields, examples & errors
  • No query fields. Counts each tag once per post; ties sort by tag.

Request

Shell
curl --fail-with-body --silent --show-error -X GET "https://surgr.ai/api/v1/tags" \
  -H "Authorization: Bearer $SURGR_API_KEY"

Success example · HTTP 200

JSON
[
  {
    "tag": "launch",
    "count": 1
  }
]

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.

GET/api/v1/timeline

Read scoped scheduled, publishing and published posts ordered by scheduledAt.

PermissionNo action permission; valid scoped key required for reads.

Fields, examples & errors

Parameters

FieldTypeRequiredDetails
fromstringNoOptional inclusive scheduledAt lower bound; ISO datetime with offset.
tostringNoOptional inclusive scheduledAt upper bound; ISO datetime with offset.
fromOptional
string

Optional inclusive scheduledAt lower bound; ISO datetime with offset.

toOptional
string

Optional inclusive scheduledAt upper bound; ISO datetime with offset.

  • Only posts with a scheduledAt are returned. Bounds are inclusive; no default range or pagination. Raw post records include threadParts, selected target delivery fields and _count.threadParts/_count.targets.

Request

Shell
curl --fail-with-body --silent --show-error -X GET "https://surgr.ai/api/v1/timeline" \
  -H "Authorization: Bearer $SURGR_API_KEY"

Success example · HTTP 200

JSON
[]

Errors

401
Missing, invalid, expired or revoked bearer key.
429
Configured rate limit exceeded or client IP unavailable.
500
Handler or middleware failed.
400
Invalid from/to datetime.

Prefer tools to HTTP?

Connect through MCP with the same key and permissions. All documentation.