Create post
Creates a post with one item per network (queue, fixed time or draft). Each item may carry channel (the account on that network; without it, the default channel). With kind: "image" it is a single-photo post, Instagram only (through Buffer or directly through Meta).
Authorization: Bearer praeko_… and X-Praeko-Contract: en. Always send both.Parameters
| Name | In | Description |
|---|---|---|
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. |
Request body
JSON. Required fields must be sent; the others can be left out. The server returns 422 invalid_request with the full list of problems in details.errors. The media must be ready (completed). Image post (kind: image): a single photo on Instagram, through Buffer or directly through Meta. The photo is a type: image media item (JPEG); Instagram takes JPEG up to 8 MB with an aspect ratio from 4:5 to 1.91:1 (use 1080x1350). The item may come back with warnings (stable code): alt_ignored (Buffer has no alt text), image_small, image_will_be_resized, image_dimensions_unknown. The post comes back with kind and media.images.
| Field | Type | Description |
|---|---|---|
title (optional) |
string | Post title. Required unless texts.youtube.title is present. Up to 200 characters. |
baseText (optional) |
string | Base text, stored with the post. |
kind (optional) |
string | video (default) or image: a single photo, Instagram only. YouTube and TikTok refuse image (422 kind_not_supported_by_network). Values: video, image; default: video. |
media required |
object | Media of the post (ids from POST /v1/media, already completed). |
media.video (optional) |
string | Id of the video media item. Required when kind is video. |
media.cover (optional) |
string | Id of the cover media item (PNG or JPEG). Video posts only. |
media.images (optional) |
object[] | Only with kind: image: the photo of the post, ONE for now (several would be a carousel, which arrives in a later phase: 422 carousel_not_supported). Up to 1 items. |
media.images[].media required |
string | Id of a completed type: image media item. |
media.images[].alt (optional) |
string | Alternative text (up to 1000 characters). Sent to Instagram when the item publishes through Meta (via: meta); through Buffer it is ignored and the item comes back with the alt_ignored warning. Up to 1000 characters. |
texts required |
object | Texts per network. Each network in items needs its own text. |
texts.youtube (optional) |
object | YouTube texts. |
texts.youtube.title (optional) |
string | Video title (up to 100 characters; longer text is truncated). |
texts.youtube.text required |
string | Description. |
texts.youtube.pinnedComment (optional) |
string | Pinned comment, posted after the video becomes public. |
texts.instagram (optional) |
object | Instagram texts. |
texts.instagram.text required |
string | Caption: up to 2200 characters and 30 hashtags. |
texts.instagram.pinnedComment (optional) |
string | Pinned comment. |
texts.tiktok (optional) |
object | TikTok texts. |
texts.tiktok.text required |
string | Caption: up to 2200 characters. |
items required |
object[] | One item per network (a network cannot repeat). |
items[].network required |
string | Network of the item. Values: youtube, instagram, tiktok. |
items[].channel (optional) |
string | Id of the account on that network (from GET /v1/channels). Without it, the default channel. |
items[].mode (optional) |
string | How to publish. YouTube: private, scheduled or public. TikTok: draft or direct. Instagram: now. Values: private, scheduled, public, draft, direct, now; default: private on YouTube, draft on TikTok, now on Instagram. |
items[].scheduledFor (optional) |
string | Fixed publishing time. ISO 8601 date and time in UTC, for example 2026-11-14T20:30:00Z. At least 30 s ahead (Instagram: 5 min through Buffer, 25 min through Meta; YouTube scheduled: 5 min). |
items[].queue (optional) |
boolean | true uses the channel's next free time slot (see PUT /v1/channels/:network/slots). |
items[].via (optional) |
string | Instagram only: which route to publish through. Values: meta, buffer. |
items[].replaces (optional) |
object | Instagram with via: meta only: deletes this post from Buffer when publishing. |
items[].replaces.provider required |
string | Always buffer. Values: buffer. |
items[].replaces.id required |
string | Id of the post in Buffer. |
items[].media (optional) |
object | Media for this item only (replace the post's media). Only video and cover: images per item arrive in a later phase. |
items[].media.video (optional) |
string | Id of the video media item. |
items[].media.cover (optional) |
string | Id of the cover media item. |
queue (optional) |
boolean | Applies to every item that has neither queue nor scheduledFor. |
draft (optional) |
boolean | true creates everything as a draft (no time required). Default: false. |
options (optional) |
object | Per-network and general options. |
options.format (optional) |
string | long marks the post as a long-form video (without it, it is a Short). Values: long. |
options.youtube (optional) |
object | YouTube options. |
options.youtube.mode (optional) |
string | Same as items[].mode. Values: private, scheduled, public. |
options.youtube.categoryId (optional) |
string | Video category. Default: 28. |
options.youtube.tags (optional) |
string[] | Video tags. |
options.youtube.notifySubscribers (optional) |
boolean | Notify subscribers. |
options.youtube.comment (optional) |
boolean | false skips posting the pinned comment. |
options.tiktok (optional) |
object | TikTok options. |
options.tiktok.mode (optional) |
string | Same as items[].mode. Values: draft, direct. |
options.tiktok.privacy (optional) |
string | Who can view it. Values: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY; default: SELF_ONLY. |
options.tiktok.coverFrameMs (optional) |
integer | Video frame used as the cover, in milliseconds. Minimum 0. |
options.tiktok.disableComments (optional) |
boolean | Turn off comments. |
options.tiktok.disableDuet (optional) |
boolean | Turn off Duet. |
options.tiktok.disableStitch (optional) |
boolean | Turn off Stitch. |
options.tiktok.commercialContent (optional) |
boolean | Commercial content (requires ownBrand and/or thirdPartyBrand). |
options.tiktok.ownBrand (optional) |
boolean | Promotes your own brand. |
options.tiktok.thirdPartyBrand (optional) |
boolean | Promotes a third-party brand (cannot be SELF_ONLY in direct mode). |
options.instagram (optional) |
object | Instagram options. |
options.instagram.via (optional) |
string | Which route to publish through. Values: meta, buffer. |
options.instagram.shareToFeed (optional) |
boolean | Also show it in the feed. Default: true. |
options.instagram.coverFrameMs (optional) |
integer | Video frame used as the cover, in milliseconds. Minimum 0. |
tags (optional) |
string[] | Post tags (up to 40 characters each). Each up to 40 characters. |
id (optional) |
string | An id you choose (3 to 64 characters among letters, digits, - and _). Without it, the server generates one. A repeated id returns 409 id_exists. |
origin (optional) |
string | Who created the post (free text; default api). |
{
"title": "The test that only failed on Mondays",
"media": {
"video": "01KA3VQ7M2T9XE4HB8N5RZ6C0D"
},
"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"
}
},
"items": [
{
"network": "youtube",
"mode": "scheduled",
"scheduledFor": "2026-11-10T20:30:00.000Z"
}
],
"tags": [
"behind-the-scenes"
]
}Example
curl -X POST "https://praeko.app/v1/posts" \
-H "Authorization: Bearer $PRAEKO_KEY" \
-H "X-Praeko-Contract: en" \
-H "Content-Type: application/json" \
-d '{
"title": "The test that only failed on Mondays",
"media": {
"video": "01KA3VQ7M2T9XE4HB8N5RZ6C0D"
},
"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"
}
},
"items": [
{
"network": "youtube",
"mode": "scheduled",
"scheduledFor": "2026-11-10T20:30:00.000Z"
}
],
"tags": [
"behind-the-scenes"
]
}'const res = await fetch("https://praeko.app/v1/posts", {
method: "POST",
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",
"media": {
"video": "01KA3VQ7M2T9XE4HB8N5RZ6C0D"
},
"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"
}
},
"items": [
{
"network": "youtube",
"mode": "scheduled",
"scheduledFor": "2026-11-10T20:30:00.000Z"
}
],
"tags": [
"behind-the-scenes"
]
}),
});
console.log(res.status, await res.json());import { Praeko } from "@praeko/sdk";
const praeko = new Praeko({ url: "https://praeko.app", key: process.env.PRAEKO_KEY });
const resultado = await praeko.posts.create({
"title": "The test that only failed on Mondays",
"media": {
"video": "01KA3VQ7M2T9XE4HB8N5RZ6C0D"
},
"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"
}
},
"items": [
{
"network": "youtube",
"mode": "scheduled",
"scheduledFor": "2026-11-10T20:30:00.000Z"
}
],
"tags": [
"behind-the-scenes"
]
});
console.log(resultado);import os
import requests
resp = requests.post(
"https://praeko.app/v1/posts",
headers={
"Authorization": f"Bearer {os.environ['PRAEKO_KEY']}",
"X-Praeko-Contract": "en",
},
json={
"title": "The test that only failed on Mondays",
"media": {
"video": "01KA3VQ7M2T9XE4HB8N5RZ6C0D",
},
"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",
},
},
"items": [
{
"network": "youtube",
"mode": "scheduled",
"scheduledFor": "2026-11-10T20:30:00.000Z",
},
],
"tags": [
"behind-the-scenes",
],
},
)
print(resp.status_code, resp.json())<?php
$corpo = <<<'JSON'
{
"title": "The test that only failed on Mondays",
"media": {
"video": "01KA3VQ7M2T9XE4HB8N5RZ6C0D"
},
"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"
}
},
"items": [
{
"network": "youtube",
"mode": "scheduled",
"scheduledFor": "2026-11-10T20:30:00.000Z"
}
],
"tags": [
"behind-the-scenes"
]
}
JSON;
$ch = curl_init('https://praeko.app/v1/posts');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
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);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
body := strings.NewReader(`{
"title": "The test that only failed on Mondays",
"media": {
"video": "01KA3VQ7M2T9XE4HB8N5RZ6C0D"
},
"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"
}
},
"items": [
{
"network": "youtube",
"mode": "scheduled",
"scheduledFor": "2026-11-10T20:30:00.000Z"
}
],
"tags": [
"behind-the-scenes"
]
}`)
req, err := http.NewRequest("POST", "https://praeko.app/v1/posts", 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))
}require "net/http"
require "uri"
uri = URI("https://praeko.app/v1/posts")
request = Net::HTTP::Post.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",
"media": {
"video": "01KA3VQ7M2T9XE4HB8N5RZ6C0D"
},
"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"
}
},
"items": [
{
"network": "youtube",
"mode": "scheduled",
"scheduledFor": "2026-11-10T20:30:00.000Z"
}
],
"tags": [
"behind-the-scenes"
]
}
JSON
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.bodyusing System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "https://praeko.app/v1/posts");
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",
"media": {
"video": "01KA3VQ7M2T9XE4HB8N5RZ6C0D"
},
"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"
}
},
"items": [
{
"network": "youtube",
"mode": "scheduled",
"scheduledFor": "2026-11-10T20:30:00.000Z"
}
],
"tags": [
"behind-the-scenes"
]
}
""", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");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",
"media": {
"video": "01KA3VQ7M2T9XE4HB8N5RZ6C0D"
},
"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"
}
},
"items": [
{
"network": "youtube",
"mode": "scheduled",
"scheduledFor": "2026-11-10T20:30:00.000Z"
}
],
"tags": [
"behind-the-scenes"
]
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://praeko.app/v1/posts"))
.header("Authorization", "Bearer " + System.getenv("PRAEKO_KEY"))
.header("X-Praeko-Contract", "en")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}http POST "https://praeko.app/v1/posts" \
"Authorization: Bearer $PRAEKO_KEY" \
"X-Praeko-Contract: en" \
"Content-Type: application/json" \
--raw='{
"title": "The test that only failed on Mondays",
"media": {
"video": "01KA3VQ7M2T9XE4HB8N5RZ6C0D"
},
"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"
}
},
"items": [
{
"network": "youtube",
"mode": "scheduled",
"scheduledFor": "2026-11-10T20:30:00.000Z"
}
],
"tags": [
"behind-the-scenes"
]
}'npm install @praeko/sdk (see SDK and CLI).Response
| Success status | When |
|---|---|
201 |
Success. |
{
"id": "01KA3W2Y6H1RC5ZQ8M0T4VXE7N",
"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"
}
]
}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. |
| 422 | invalid_request |
Something is missing; the full list comes in details.errors. |
| 422 | invalid_field |
Invalid id, channel, mode or tag. |
| 422 | time_in_past |
The time has already passed or is too close. |
| 422 | missing_schedule |
Missing scheduledFor, queue or draft. |
| 409 | channel_not_connected |
The network is not connected. |
| 409 | no_slots |
queue on a channel without time slots. |
| 409 | id_exists |
A post with this id already exists. |
| 409 | media_not_ready |
The media upload has not been completed yet. |
| 422 | media_not_found |
The media item does not exist. |
| 422 | media_type_mismatch |
The media item has the wrong type (kind: image needs type: image). |
| 422 | kind_not_supported_by_network |
kind: image on a YouTube or TikTok item. |
| 422 | carousel_not_supported |
More than one image in media.images (carousel arrives later). |
| 422 | invalid_image |
The image is not a JPEG (Instagram only takes JPEG). |
| 422 | image_too_large |
The image is over 8 MB (Instagram limit). |
| 422 | image_aspect_ratio |
The image aspect ratio is outside 4:5 to 1.91:1 (Instagram would refuse it; use 1080x1350). |
| 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.
Try it
The live test uses your key and your account. It stays off when you are not signed in.