> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sapt.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Publishing now

> Publish immediately, track delivery, and retry a failure.

## Publish on creation

Set `publishNow: true` and omit `scheduledFor`:

```json theme={null}
{
  "socialAccountId": "9f1c2f7a-…",
  "platform": "instagram",
  "mediaType": "STORIES",
  "caption": "Live now.",
  "uploadedHandles": [{ "stagingId": "stg_01JQ…", "ext": "mp4" }],
  "publishNow": true,
  "timezone": "UTC"
}
```

## Publish something already on the calendar

```bash theme={null}
curl -X POST \
  "https://api.sapt.ai/socials/posts/$PROJECT_ID/$POST_ID/publish" \
  -H "Authorization: ApiKey $SAPT_API_KEY"
```

This bypasses `scheduledFor` and sends the post now.

## Track delivery

Publishing is not always synchronous — media has to be uploaded to the platform
and processed. Poll for the outcome:

```bash theme={null}
curl "https://api.sapt.ai/socials/posts/$PROJECT_ID/$POST_ID/status" \
  -H "Authorization: ApiKey $SAPT_API_KEY"
```

```typescript theme={null}
async function waitForPublish(projectId: string, postId: string) {
  const terminal = new Set(['published', 'failed'])

  for (let attempt = 0; attempt < 40; attempt++) {
    const res = await fetch(
      `https://api.sapt.ai/socials/posts/${projectId}/${postId}/status`,
      { headers: { Authorization: `ApiKey ${process.env.SAPT_API_KEY}` } }
    )
    const { data } = await res.json()

    if (terminal.has(data.status)) return data

    // Back off: platform video processing routinely takes minutes.
    await new Promise((r) => setTimeout(r, Math.min(2000 * 2 ** attempt, 30_000)))
  }

  throw new Error('Timed out waiting for publish')
}
```

When it lands, `status` is `published` and `permalink` points at the live post.

## When it fails

A failure sets `status: "failed"` and puts the reason in `errorMessage`:

```json theme={null}
{
  "success": true,
  "data": {
    "status": "failed",
    "errorMessage": "Instagram rejected the media: aspect ratio must be between 4:5 and 1.91:1",
    "permalink": null
  }
}
```

Retry by calling **publish** again — publishing a `failed` post re-runs delivery
from the failed state. There is no separate retry endpoint.

```bash theme={null}
curl -X POST \
  "https://api.sapt.ai/socials/posts/$PROJECT_ID/$POST_ID/publish" \
  -H "Authorization: ApiKey $SAPT_API_KEY"
```

<Warning>
  Check `permalink` before retrying. If a post reached the platform but Sapt
  failed to record the result, it will be `failed` with a non-null `permalink` —
  retrying that one double-posts. Fix the underlying cause (usually media that
  violates a platform rule) before retrying a genuine failure.
</Warning>

## Two publish paths

Which one runs is an implementation detail, but it explains the statuses you'll
see:

* **Async (queued)** — `draft → queued → publishing → published`. The default
  for most publishes.
* **Sync fallback** — `draft → uploading → published`. The request holds until
  the platform responds.

Either way, `published` is the only status that means it is live, and `failed`
is the only other terminal state.
