# From zero to your first published video

> 7 steps with the Praeko API: account, API key, first request, your first video (SDK, CLI or agent), media, scheduling and a webhook, with examples in 10 languages.

Source: https://praeko.app/docs/en/get-started

7 steps: account, key, first request, your first video, media, scheduling and a webhook. The examples use `curl`; switch the tab to see the same step in another language.

> **Prefer to do it with an AI agent?** The [Get started with your AI agent](https://praeko.app/docs/en/get-started-with-agent) guide takes you from zero to your first video with one prompt: the agent does almost everything and you approve and do what only you can do.

### 1. Create your account

Sign in with Google at `praeko.app` and connect at least one channel (YouTube, Instagram or TikTok) under Channels.

> **Waitlist access** Praeko is a closed beta today: the [Join the waitlist](https://praeko.app/#lista) button takes you to the form.

### 2. Create an API key

In the panel, go to Developers, API keys. Pick the smallest scope that works: each level includes the one before.

| Scope | Use it to |
|---|---|
| (read) | Read channels, posts, metrics, comments and usage. |
| (write) | Everything in read, plus create and cancel posts, upload media and reply to comments. |
| (admin) | Everything in write, plus keys, webhooks, channel connections and imports. |

> **The secret is shown once** Copy the `praeko_…` key when you create it and keep it in an environment variable: `export PRAEKO_KEY=…`

### 3. Make your first request

List your channels. If you get the list back, the key and the header are right.

**curl**

```bash
curl "https://praeko.app/v1/channels" \
  -H "Authorization: Bearer $PRAEKO_KEY" \
  -H "X-Praeko-Contract: en"
```

**JavaScript**

```js
const res = await fetch("https://praeko.app/v1/channels", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.PRAEKO_KEY}`,
    "X-Praeko-Contract": "en",
  },
});
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.channels.list();
console.log(resultado);
```

**Python**

```python
import os
import requests

resp = requests.get(
    "https://praeko.app/v1/channels",
    headers={
        "Authorization": f"Bearer {os.environ['PRAEKO_KEY']}",
        "X-Praeko-Contract": "en",
    },
)
print(resp.status_code, resp.json())
```

**PHP**

```php
<?php
$ch = curl_init('https://praeko.app/v1/channels');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PRAEKO_KEY"),
        'X-Praeko-Contract: en',
    ],
]);
$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"
)

func main() {
	req, err := http.NewRequest("GET", "https://praeko.app/v1/channels", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("PRAEKO_KEY"))
	req.Header.Set("X-Praeko-Contract", "en")
	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/channels")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV['PRAEKO_KEY']}"
request["X-Praeko-Contract"] = "en"

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.Get, "https://praeko.app/v1/channels");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("PRAEKO_KEY"));
request.Headers.Add("X-Praeko-Contract", "en");

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 {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://praeko.app/v1/channels"))
            .header("Authorization", "Bearer " + System.getenv("PRAEKO_KEY"))
            .header("X-Praeko-Contract", "en")
            .method("GET", HttpRequest.BodyPublishers.noBody())
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

**HTTPie**

```bash
http GET "https://praeko.app/v1/channels" \
  "Authorization: Bearer $PRAEKO_KEY" \
  "X-Praeko-Contract: en"
```

**Response 200**

```json
{
  "channels": [
    {
      "network": "youtube",
      "supported": true,
      "id": "01KA3V0R5D8M2WQ7X4HT9NZ1BF",
      "connected": true,
      "detail": "Canal do Marcone",
      "name": "Canal do Marcone",
      "timezone": "America/Sao_Paulo",
      "slots": 3,
      "avatar": "/v1/channels/youtube/avatar",
      "default": true
    },
    {
      "network": "instagram",
      "supported": true,
      "id": "01KA3V1G4C7N9PXS2WQ6HM8TDZ",
      "connected": true,
      "detail": "via API da Meta",
      "timezone": "America/Sao_Paulo",
      "slots": 0,
      "meta": {
        "connected": true,
        "username": "dev.ocioso"
      },
      "name": "dev.ocioso"
    },
    {
      "network": "tiktok",
      "supported": true,
      "connected": false,
      "detail": "não conectado (faltam os secrets TIKTOK_CLIENT_KEY e TIKTOK_CLIENT_SECRET)"
    }
  ]
}
```

### 4. Your first video

**Do not have the video yet? Make it with Praeko Studio**, using your own coding agent (Claude Code, Codex, Gemini CLI, Cursor, opencode). Then publish with the paths below.

**Praeko Studio**

```bash
npm install -g @praeko/studio
praeko-studio doctor
praeko-studio init my-channel --agent claude
cd my-channel   # open your agent here and ask for the video
```

Full guide, with the text to paste into your agent, licenses (Remotion is not MIT) and how to publish: [Praeko Studio](https://praeko.app/docs/en/studio). Agent without a terminal (such as Claude Desktop)? Use the [local Studio MCP](https://praeko.app/docs/en/studio/mcp): it makes the video and uploads the files, but publishing is done by the remote `praeko` MCP server.

**I already have the video:** publish with the SDK, the CLI or your agent (MCP).

You have a finished vertical video (MP4 or MOV, 9:16 for Shorts, Reels and TikTok) and want to see it published. There are three ways in and all three reach the same API. The example uses `clipe.mp4`, a 20 s clip (1080×1920, 18 MiB) that goes up as **private** on YouTube first, so you can check it before making it public.

> **Before you start** Connect a YouTube channel under Channels in the panel, and keep the key from step 2 in `PRAEKO_KEY`.

**With the SDK, in TypeScript or JavaScript.** The package ships its TypeScript types and has no dependencies.

**Install**

```bash
npm install @praeko/sdk
```

**publicar.mjs**

```js
// publicar.mjs: node publicar.mjs
import { Praeko } from "@praeko/sdk";
import { uploadFile } from "@praeko/sdk/node"; // Node only: reads the file from disk in parts

const praeko = new Praeko({ url: "https://praeko.app", key: process.env.PRAEKO_KEY });

// 1) upload the video (up to 50 MiB goes in a single request; above that the SDK splits it into parts)
const video = await uploadFile(praeko, "./clipe.mp4", { type: "video" });

// 2) create the post: one item per network. On YouTube, mode "private" uploads now and keeps the video private
const post = await praeko.posts.create(
  {
    title: "How a cache brought down the deploy",
    media: { video: video.id },
    texts: { youtube: { title: "How a cache brought down the deploy", text: "20 seconds on the bug that only showed up after the deploy.\n\n#programming #shorts" } },
    items: [{ network: "youtube", mode: "private" }],
  },
  { idempotencyKey: "first-video-clipe-01" }, // running the script twice does not create a second post (the key lasts 24 h)
);

console.log(post.id, post.items[0].state);
```

The same code works in `.ts`: run it with the TypeScript runner you already use. `uploadFile` returns the ready media; `posts.create` returns the post:

**Ready media (upload response)**

```json
{
  "id": "01KA3VQ7M2T9XE4HB8N5RZ6C0D",
  "type": "video",
  "name": "clipe.mp4",
  "mime": "video/mp4",
  "size": 18874368,
  "state": "ready",
  "publicUrl": "https://praeko.app/v1/m/01KA3VQ7M2T9XE4HB8N5RZ6C0D/k7Qm2vXz9Rt4Lp8Wc3Nd6Hy1Bs5Fa0J/clipe.mp4",
  "durationMs": 20040,
  "width": 1080,
  "height": 1920,
  "createdAt": "2026-11-07T14:02:11.482Z"
}
```

**Response 201 from POST /v1/posts**

```json
{
  "id": "01KA3W2Y6H1RC5ZQ8M0T4VXE7N",
  "items": [
    {
      "id": "01KA3W2Y6J9D3BNF7G2K5PQS8A",
      "network": "youtube",
      "channel": "01KA3V0R5D8M2WQ7X4HT9NZ1BF",
      "state": "scheduled",
      "scheduledFor": null,
      "custom": false,
      "mode": "private",
      "runAt": "2026-11-07T14:02:12.000Z"
    }
  ]
}
```

The item starts with `state: "scheduled"` and `runAt` set to now; Praeko then uploads the video to YouTube. When the item moves to `private`, the video is on YouTube, private, waiting for you. Check it with `GET /v1/posts/:id` and, when you are happy, schedule it with `praeko.posts.scheduleYoutube(post.id, "2026-11-14T17:30:00Z")` or make it public in YouTube Studio.

**With the CLI.** The CLI reads a package folder with the video, the cover and a `publication.json`, and checks the texts before sending. It also needs a `praeko.json` in the current folder (or above).

**The package folder**

```text
package/
  clipe.mp4          # the video
  cover.png          # the cover (the name must contain capa, cover or thumb)
  publication.json   # title, the text for each network and hashtags
```

**package/publication.json**

```json
{
  "title": "How a cache brought down the deploy",
  "format": "short",
  "files": { "video": "clipe.mp4", "cover": "cover.png" },
  "yt": { "description": "20 seconds on the bug that only showed up after the deploy." },
  "ig": { "caption": "The bug that only showed up after the deploy." },
  "tiktok": { "caption": "The bug that only showed up after the deploy." },
  "hashtags": { "youtube": ["#programming", "#shorts"], "instagram": ["#programming"], "tiktok": ["#programming"] }
}
```

**praeko.json**

```json
{
  "name": "My channel",
  "links": { "main": "https://example.com" }
}
```

In `praeko.json`, `links.main` is optional: it is the link the CLI appends, with UTM tags, to the end of the YouTube description. The text for each network comes from `publication.json`.

**Terminal**

```bash
npm install -g @praeko/cli        # or use npx @praeko/cli in place of praeko
export PRAEKO_SERVER=https://praeko.app
export PRAEKO_KEY="praeko_…"

praeko post ./package --networks yt --yt-mode private           # SIMULATES: nothing is sent
praeko post ./package --networks yt --yt-mode private --send    # sends for real
```

**Simulation output**

```text
SIMULATION (nothing will be sent; use --send) [remote]: How a cache brought down the deploy
package: /home/you/package
video: clipe.mp4  cover: cover.png
  YouTube    now  mode private  text 126 chars
```

For all three networks at once, use `--when "2026-11-14T17:30" --networks yt,ig,tt` (time in the `praeko.json` time zone, `-03:00` by default) or `--queue`, which takes the next free slot of each channel. Without `--send` the CLI only simulates. All commands are in [SDK and CLI](https://praeko.app/docs/en/sdk#cli).

**With your agent, over MCP.** Connect Claude Code, Codex or another agent to the `https://praeko.app/mcp` server (with an API key or by signing in with your account) and ask: "publish `~/clips/clipe.mp4` as a private YouTube video titled …". The agent shows a preview with a confirmation code before anything is published, and uploads the file from disk with the upload tools. See [MCP server](https://praeko.app/docs/en/mcp) and [AI agents](https://praeko.app/docs/en/agents).

Do not have the video yet? See [Generate the video with Praeko Studio](#studio).

### 5. Upload the media, under the SDK

The SDK, the CLI and your agent do this for you. Create the media, send the bytes and use the `id` in the post. Up to 50 MiB you can send the whole file with `simple: true`; above that, send parts of at least 5 MiB and then `POST /v1/media/:id/complete`. Details in [Media upload](https://praeko.app/docs/en/guides/media).

**curl**

```bash
curl -X POST https://praeko.app/v1/media -H "Authorization: Bearer $PRAEKO_KEY" \
  -H "X-Praeko-Contract: en" -H "Content-Type: application/json" \
  -d '{ "type": "video", "name": "v.mp4", "mime": "video/mp4", "size": 8123456, "simple": true }'
# response: { "id": "…", "state": "uploading", … }
curl -X PUT https://praeko.app/v1/media/$MEDIA_ID/content -H "Authorization: Bearer $PRAEKO_KEY" \
  -H "Content-Type: video/mp4" --data-binary @v.mp4
```

### 6. Schedule a post, under the SDK

One item per network. Each item goes to the queue (`queue`), to a fixed time (`scheduledFor`) or stays a draft (`draft`). On YouTube and TikTok, an item with no time goes up now.

**curl**

```bash
curl -X POST https://praeko.app/v1/posts -H "Authorization: Bearer $PRAEKO_KEY" \
  -H "X-Praeko-Contract: en" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "title": "My video", "media": { "video": "'$MEDIA_ID'" },
        "texts": { "youtube": { "text": "Description" } },
        "items": [ { "network": "youtube", "queue": true } ] }'
```

### 7. Listen for a webhook

Register a public https URL (admin scope). The `whsec_…` secret comes back once. Each delivery carries `Praeko-Signature`, signed with HMAC-SHA256.

> **Done** When the post goes out you receive `post.sent`. Next: [verify the signature](https://praeko.app/docs/en/guides/webhooks) and handle repeats.

## Generate the video with Praeko Studio

Praeko Studio is Praeko's video-as-code tool: you write the script and it builds a vertical Short. The path is: **script** (plain text) → **voice** (narration with ElevenLabs or a local voice; there is a fast, silent draft mode just for laying out scenes) → word-by-word **captions** with the timing of each word → **scenes** (interface pieces described in YAML and TSX, rendered with Remotion or HyperFrames) → **cover** → **package**, the folder with the video, the cover and the `publication.json` that the CLI and the SDK know how to publish. The commands are `praeko studio new`, `voice`, `render`, `cover` and `package`.

> **Install and start** `npm install -g @praeko/studio` and follow the [Praeko Studio](https://praeko.app/docs/en/studio) guide: requirements, your first video in 10 minutes with your agent, keys, licenses and how to publish through Praeko. Without a terminal, use the [local Studio MCP](https://praeko.app/docs/en/studio/mcp) (`@praeko/studio-mcp`, which never publishes). Remotion, the default engine, is **not MIT**: free for companies of up to 3 people.
