# Update post

> Update post: PATCH /v1/posts/:id. Edits the title, texts, media, tags and items, or moves the post to drafts or to the queue. Send only what changed; everything else stays. A YouTube item that is already uploaded and still scheduled can also change its time and metadata (checked live on YouTube).

Source: https://praeko.app/docs/en/api/posts/update-post

`PATCH /v1/posts/:id` · minimum scope: write

Edits the title, texts, media, tags and items, or moves the post to drafts or to the queue. Send only what changed; everything else stays. A YouTube item that is already uploaded and still scheduled can also change its time and metadata (checked live on YouTube).

> **Headers** `Authorization: Bearer praeko_…` and `X-Praeko-Contract: en`. Always send both.

## Parameters

| Name | In | Description |
|---|---|---|
| `id` | path | Post id. |
| `Idempotency-Key` | header | Idempotency key (1 to 255 visible characters, for example a UUID). Repeating the SAME request with the same key within 24 h returns the stored response (with `Idempotent-Replayed: true`) without running it again; a different request with the same key returns 422 `idempotency_key_reuse`; a request still in progress returns 409 `idempotency_in_progress`. 5xx errors are not stored. The key is scoped per workspace and per route. See [Retry a request safely](https://praeko.app/docs/en/guides/idempotency). |

## Request body

JSON. Required fields must be sent; the others can be left out. Send only what changes. A post adopted from Buffer only accepts text, time and draft changes (anything else returns 409 `post_adopted`). **YouTube, already uploaded:** a YouTube item in the `scheduled` state (Praeko has already uploaded the video as private, with `publishAt` in the future) or `private` can change `scheduledFor` (it becomes `videos.update` with `status.publishAt`; the rest of the status is preserved), title, description (`texts.youtube`), `videoTags`, `categoryId` and the `pinnedComment` that has not been posted yet. Before acting, the server checks LIVE on YouTube (`videos.list`, 1 quota unit; each write costs 50). If the video was already published, the scheduled time has passed, the video was deleted or it was not uploaded by Praeko, nothing is written and the response is 409 with the reason and the Studio link (`https://studio.youtube.com/video/<id>/edit`). The new time must be at least 5 minutes from now (otherwise 422 `time_in_past`). Asking for the same time again does not call YouTube. A PATCH with several items applies each one independently and returns `itemResults` (one per already uploaded YouTube item: `applied`, `changed`, `fields`, `notApplied`, `reason`, `message`, `publishAt`, `studio`); if NONE of the requested items could change, the response is the 409 of the first one. Pending Instagram and TikTok items follow the usual rules.

| Field | Type | Description |
|---|---|---|
| `title` (optional) | string | New title. 1 to 200 characters. |
| `texts` (optional) | object | Per-network texts that change. The fields you send replace the current ones; the others stay. A `text` cannot be empty or contain a placeholder such as `[LINK_DO_LONGO]`. |
| `texts.youtube` (optional) | object | YouTube texts. |
| `texts.youtube.title` (optional) | string | Title. |
| `texts.youtube.text` (optional) | string | Description. |
| `texts.youtube.pinnedComment` (optional) | string | Pinned comment. |
| `texts.instagram` (optional) | object | Instagram texts. |
| `texts.instagram.text` (optional) | string | Caption. |
| `texts.instagram.pinnedComment` (optional) | string | Pinned comment. |
| `texts.tiktok` (optional) | object | TikTok texts. |
| `texts.tiktok.text` (optional) | string | Caption. |
| `media` (optional) | object | Media replacement. |
| `media.cover` (optional) | string | Id of the new cover (or `null` to remove it). |
| `media.video` (optional) | string | Id of the new video (only for an adopted post or one still in draft; otherwise 409 `media_locked`). |
| `tags` (optional) | string[] | Tags (replace the current ones). |
| `items` (optional) | object[] | Items to edit, selected by `id` or by `network`. Only items that are still pending can be edited. |
| `items[].id` (optional) | string | Item id. |
| `items[].network` (optional) | string | Network of the item (alternative to `id`). Values: youtube, instagram, tiktok. |
| `items[].mode` (optional) | string | New mode (YouTube: private, scheduled, public; TikTok: draft, direct). Values: private, scheduled, public, draft, direct, now. |
| `items[].scheduledFor` (optional) | string | New fixed time. ISO 8601 date and time in UTC, for example `2026-11-14T20:30:00Z`. |
| `items[].queue` (optional) | boolean | Goes back to the channel's next free time slot. |
| `items[].draft` (optional) | boolean | Turns it into a draft. |
| `items[].privacy` (optional) | string | TikTok: who can view it. Values: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. |
| `items[].coverFrameMs` (optional) | integer | TikTok: cover frame in milliseconds. Minimum 0. |
| `items[].videoTags` (optional) | string[] | YouTube, only for a video already uploaded and still scheduled: video tags (replace the current ones). |
| `items[].categoryId` (optional) | string | YouTube, only for a video already uploaded and still scheduled: category number (e.g. `28`). |
| `moveTo` (optional) | string | Moves ALL editable items to drafts or to the queue. Values: draft, queue. |

**Body example**

```json
{
  "title": "The test that only failed on Mondays (final cut)",
  "tags": [
    "behind-the-scenes",
    "testing"
  ],
  "items": [
    {
      "network": "youtube",
      "scheduledFor": "2026-11-10T20:30:00.000Z"
    }
  ]
}
```

## Example

**curl**

```bash
curl -X PATCH "https://praeko.app/v1/posts/01KA3W2Y6H1RC5ZQ8M0T4VXE7N" \
  -H "Authorization: Bearer $PRAEKO_KEY" \
  -H "X-Praeko-Contract: en" \
  -H "Content-Type: application/json" \
  -d '{
  "title": "The test that only failed on Mondays (final cut)",
  "tags": [
    "behind-the-scenes",
    "testing"
  ],
  "items": [
    {
      "network": "youtube",
      "scheduledFor": "2026-11-10T20:30:00.000Z"
    }
  ]
}'
```

**JavaScript**

```js
const res = await fetch("https://praeko.app/v1/posts/01KA3W2Y6H1RC5ZQ8M0T4VXE7N", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.PRAEKO_KEY}`,
    "X-Praeko-Contract": "en",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "title": "The test that only failed on Mondays (final cut)",
    "tags": [
      "behind-the-scenes",
      "testing"
    ],
    "items": [
      {
        "network": "youtube",
        "scheduledFor": "2026-11-10T20:30:00.000Z"
      }
    ]
  }),
});
console.log(res.status, await res.json());
```

**Node**

```js
import { Praeko } from "@praeko/sdk";

const praeko = new Praeko({ url: "https://praeko.app", key: process.env.PRAEKO_KEY });
const resultado = await praeko.posts.update("01KA3W2Y6H1RC5ZQ8M0T4VXE7N", {
  "title": "The test that only failed on Mondays (final cut)",
  "tags": [
    "behind-the-scenes",
    "testing"
  ],
  "items": [
    {
      "network": "youtube",
      "scheduledFor": "2026-11-10T20:30:00.000Z"
    }
  ]
});
console.log(resultado);
```

**Python**

```python
import os
import requests

resp = requests.patch(
    "https://praeko.app/v1/posts/01KA3W2Y6H1RC5ZQ8M0T4VXE7N",
    headers={
        "Authorization": f"Bearer {os.environ['PRAEKO_KEY']}",
        "X-Praeko-Contract": "en",
    },
    json={
        "title": "The test that only failed on Mondays (final cut)",
        "tags": [
            "behind-the-scenes",
            "testing",
        ],
        "items": [
            {
                "network": "youtube",
                "scheduledFor": "2026-11-10T20:30:00.000Z",
            },
        ],
    },
)
print(resp.status_code, resp.json())
```

**PHP**

```php
<?php
$corpo = <<<'JSON'
{
  "title": "The test that only failed on Mondays (final cut)",
  "tags": [
    "behind-the-scenes",
    "testing"
  ],
  "items": [
    {
      "network": "youtube",
      "scheduledFor": "2026-11-10T20:30:00.000Z"
    }
  ]
}
JSON;

$ch = curl_init('https://praeko.app/v1/posts/01KA3W2Y6H1RC5ZQ8M0T4VXE7N');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'PATCH',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PRAEKO_KEY"),
        'X-Praeko-Contract: en',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => $corpo,
]);
$resposta = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $resposta, PHP_EOL;
curl_close($ch);
```

**Go**

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

func main() {
	body := strings.NewReader(`{
  "title": "The test that only failed on Mondays (final cut)",
  "tags": [
    "behind-the-scenes",
    "testing"
  ],
  "items": [
    {
      "network": "youtube",
      "scheduledFor": "2026-11-10T20:30:00.000Z"
    }
  ]
}`)
	req, err := http.NewRequest("PATCH", "https://praeko.app/v1/posts/01KA3W2Y6H1RC5ZQ8M0T4VXE7N", body)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("PRAEKO_KEY"))
	req.Header.Set("X-Praeko-Contract", "en")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	out, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.Status, string(out))
}
```

**Ruby**

```ruby
require "net/http"
require "uri"

uri = URI("https://praeko.app/v1/posts/01KA3W2Y6H1RC5ZQ8M0T4VXE7N")
request = Net::HTTP::Patch.new(uri)
request["Authorization"] = "Bearer #{ENV['PRAEKO_KEY']}"
request["X-Praeko-Contract"] = "en"
request["Content-Type"] = "application/json"
request.body = <<~'JSON'
  {
    "title": "The test that only failed on Mondays (final cut)",
    "tags": [
      "behind-the-scenes",
      "testing"
    ],
    "items": [
      {
        "network": "youtube",
        "scheduledFor": "2026-11-10T20:30:00.000Z"
      }
    ]
  }
JSON

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.body
```

**C#**

```csharp
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Patch, "https://praeko.app/v1/posts/01KA3W2Y6H1RC5ZQ8M0T4VXE7N");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("PRAEKO_KEY"));
request.Headers.Add("X-Praeko-Contract", "en");
request.Content = new StringContent("""
    {
      "title": "The test that only failed on Mondays (final cut)",
      "tags": [
        "behind-the-scenes",
        "testing"
      ],
      "items": [
        {
          "network": "youtube",
          "scheduledFor": "2026-11-10T20:30:00.000Z"
        }
      ]
    }
""", Encoding.UTF8, "application/json");

using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

**Java**

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "title": "The test that only failed on Mondays (final cut)",
              "tags": [
                "behind-the-scenes",
                "testing"
              ],
              "items": [
                {
                  "network": "youtube",
                  "scheduledFor": "2026-11-10T20:30:00.000Z"
                }
              ]
            }
            """;
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://praeko.app/v1/posts/01KA3W2Y6H1RC5ZQ8M0T4VXE7N"))
            .header("Authorization", "Bearer " + System.getenv("PRAEKO_KEY"))
            .header("X-Praeko-Contract", "en")
            .header("Content-Type", "application/json")
            .method("PATCH", HttpRequest.BodyPublishers.ofString(body))
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

**HTTPie**

```bash
http PATCH "https://praeko.app/v1/posts/01KA3W2Y6H1RC5ZQ8M0T4VXE7N" \
  "Authorization: Bearer $PRAEKO_KEY" \
  "X-Praeko-Contract: en" \
  "Content-Type: application/json" \
  --raw='{
  "title": "The test that only failed on Mondays (final cut)",
  "tags": [
    "behind-the-scenes",
    "testing"
  ],
  "items": [
    {
      "network": "youtube",
      "scheduledFor": "2026-11-10T20:30:00.000Z"
    }
  ]
}'
```

> Official SDK for Node: Install it with `npm install @praeko/sdk` (see [SDK and CLI](https://praeko.app/docs/en/sdk)).

## Response

| Success status | When |
|---|---|
| `200` | Success. |

**Response 200**

```json
{
  "id": "01KA3W2Y6H1RC5ZQ8M0T4VXE7N",
  "title": "The test that only failed on Mondays (final cut)",
  "state": "queued",
  "type": "short",
  "kind": "video",
  "origin": "api",
  "media": {
    "video": "01KA3VQ7M2T9XE4HB8N5RZ6C0D",
    "cover": null
  },
  "texts": {
    "youtube": {
      "title": "The test that only failed on Mondays",
      "text": "A test that passed from Tuesday to Sunday. What it was hiding.\n\n#programming #testing"
    }
  },
  "tags": [
    "behind-the-scenes",
    "testing"
  ],
  "thumb": null,
  "items": [
    {
      "id": "01KA3W2Y6J9D3BNF7G2K5PQS8A",
      "network": "youtube",
      "channel": "01KA3V0R5D8M2WQ7X4HT9NZ1BF",
      "state": "scheduled",
      "scheduledFor": "2026-11-10T20:30:00.000Z",
      "custom": true,
      "mode": "scheduled",
      "runAt": "2026-11-10T19:30:00.000Z",
      "thumb": null
    }
  ],
  "createdAt": "2026-11-07T14:02:11.482Z",
  "updatedAt": "2026-11-07T15:40:03.120Z",
  "actions": []
}
```

## Errors

| Status | code | When |
|---|---|---|
| 400 | `empty_body` | The request body is empty. |
| 400 | `invalid_json` | The request body is not a JSON object. |
| 413 | `body_too_large` | The request body is larger than 1 MB. |
| 404 | `post_not_found` | The post does not exist. |
| 404 | `item_not_found` | The item does not exist in this post. |
| 409 | `not_editable` | The item has already gone out or is in progress (YouTube, already uploaded: it can only change while the video is still private and scheduled; once the scheduled time has passed, it can no longer change). |
| 409 | `post_adopted` | Post adopted from Buffer: only text, time and draft can change. |
| 409 | `media_locked` | The video can only be replaced in an adopted post or in a draft; the image of an image post cannot be replaced by PATCH. |
| 409 | `video_not_private` | YouTube, already uploaded: the video is already public on YouTube, so its time and metadata can only change in Studio (`details.studio` has the link). |
| 409 | `video_not_found` | YouTube, already uploaded: the video was deleted on YouTube. |
| 422 | `time_in_past` | The new time must be at least 5 minutes from now. |
| 502 | `youtube_error` | YouTube did not respond or rejected the check/write; nothing was changed (or `itemResults` says what was). |
| 422 | `invalid_field` | A field is invalid. |
| 422 | `idempotency_key_reuse` | The `Idempotency-Key` was already used with a different request (different method, path, query or body). |
| 409 | `idempotency_in_progress` | A request with the same `Idempotency-Key` is still in progress (retry in a moment). |
| 409 | `idempotency_response_too_large` | The original response was larger than 256 KB and was not stored; fetch the resource with GET instead. |

### Errors on any route

| Status | code | When |
|---|---|---|
| 401 | `unauthorized` | The `Authorization: Bearer …` header is missing. |
| 401 | `invalid_api_key` | The key does not exist. |
| 401 | `revoked_api_key` | The key has been revoked. |
| 401 | `expired_api_key` | The key has expired. |
| 403 | `forbidden` | The key's scope is not enough for this route (see the scope badge). |
| 429 | `rate_limited` | The per-minute request limit was exceeded. |

The body of every error is `{ "error": { "code", "message", "details"? } }`. See [all error codes](https://praeko.app/docs/en/guides/errors-and-limits).

## Try it

> **Try it**: the live test needs you to be signed in. It is switched off here.
