Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# BeatAPI

Official runnable examples for the BeatAPI async AI video API.
Official runnable examples for BeatAPI async workflows and Realtime Video sessions.

[![Verify examples](https://github.com/BeatAPI/beatapi-examples/actions/workflows/verify.yml/badge.svg)](https://github.com/BeatAPI/beatapi-examples/actions/workflows/verify.yml)

[Website](https://beatapi.io/) ·
[API documentation](https://docs.beatapi.io/) ·
[Realtime Video documentation](https://docs.beatapi.io/realtime-video) ·
[Music Video Playground](https://beatapi.io/music-video-api) ·
[Ecommerce Video Playground](https://beatapi.io/ecommerce-video-api)

BeatAPI gives product teams one workflow API for creating AI music videos and
ecommerce video ads. Submit media and creative direction, receive a task ID,
then poll or use a webhook until the hosted MP4 is ready.
BeatAPI gives product teams one server-side API key for two integration shapes:
async Music Video and Ecommerce Video workflows that return hosted output, and
short-lived Realtime Video sessions that connect browser media through
`@beatapi/realtime`.

The primary launch route is `POST /v1/music-video/tasks`.

Expand Down Expand Up @@ -80,6 +82,30 @@ curl https://api.beatapi.io/v1/tasks/task_8K2qA \
Stop polling when the task is `succeeded` or `failed`. Successful output URLs
are available in `data.output.media`.

### Realtime session quickstart

Create Realtime sessions only from trusted server code. The browser must never
receive the permanent `sk_...` API key. It receives only the returned,
short-lived `client_secret`:

```bash
curl https://api.beatapi.io/v1/realtime/sessions \
-X POST \
-H "Authorization: Bearer $BEATAPI_API_KEY" \
-H "Idempotency-Key: customer-call-123" \
-H "Content-Type: application/json" \
-d '{
"max_duration_seconds": 60,
"allowed_origins": ["https://app.example.com"]
}'
```

Use `GET /v1/realtime/sessions/{session_id}` to inspect the session and
`DELETE` on the same path to close it idempotently. Camera capture and WebRTC
belong in the browser SDK; the server examples manage only session lifecycle.
Realtime production access and package availability remain limited until the
published launch checks are complete.

## Examples

| Example | cURL | Node.js | Python |
Expand All @@ -89,6 +115,10 @@ are available in `data.output.media`.
| Poll a task | [`poll-task.sh`](examples/curl/poll-task.sh) | reference client | reference client |
| Upload a file | [`upload-file.sh`](examples/curl/upload-file.sh) | [`upload-file.mjs`](examples/node/upload-file.mjs) | [`upload_file.py`](examples/python/upload_file.py) |
| Receive webhooks | — | [`webhook-server.mjs`](examples/node/webhook-server.mjs) | — |
| Realtime session lifecycle | [`realtime-session.sh`](examples/curl/realtime-session.sh) | [`realtime-session.mjs`](examples/node/realtime-session.mjs) | [`realtime_session.py`](examples/python/realtime_session.py) |

The browser-side SDK handoff is shown in
[`examples/browser/realtime-video.ts`](examples/browser/realtime-video.ts).

### Node.js

Expand All @@ -98,6 +128,7 @@ verification requires Node.js 20.19+ or 22.12+.
```bash
node examples/node/music-video.mjs
node examples/node/ecommerce-video.mjs
node examples/node/realtime-session.mjs
```

The dependency-free reference client is at
Expand All @@ -112,6 +143,7 @@ Requires Python 3.11 or newer and uses only the standard library.
```bash
python3 examples/python/music_video.py
python3 examples/python/ecommerce_video.py
python3 examples/python/realtime_session.py
```

The matching reference client is at
Expand All @@ -126,6 +158,8 @@ The matching reference client is at
| `POST` | `/v1/ecommerce-video/tasks` | Create an Ecommerce Video task |
| `GET` | `/v1/tasks/{task_id}` | Poll task status and output |
| `GET` | `/v1/usage` | Read usage, credits, and concurrency |
| `POST` | `/v1/realtime/sessions` | Create a short-lived Realtime Video session |
| `GET/DELETE` | `/v1/realtime/sessions/{session_id}` | Inspect or close a Realtime Video session |
| `POST` | `/v1/files` | Upload local workflow inputs |
| `GET/POST` | `/v1/webhooks` | List or create webhook endpoints |
| `GET/PATCH/DELETE` | `/v1/webhooks/{id}` | Manage a webhook endpoint |
Expand Down Expand Up @@ -213,6 +247,9 @@ node examples/node/webhook-server.mjs
- Never commit `.env` files or paste keys into browser code.
- Never include credentials in screenshots, exported workflow JSON, or issues.
- Rotate a key immediately if it is exposed.
- For Realtime, create sessions on the server and give the browser only the
returned short-lived `client_secret`.
- Use exact HTTPS `allowed_origins`; wildcards are rejected.

## Repository scope

Expand Down
33 changes: 33 additions & 0 deletions examples/browser/realtime-video.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createRealtimeClient } from "@beatapi/realtime";

type SessionResponse = {
data: {
client_secret: string;
};
};

const response = await fetch("/api/realtime/session", { method: "POST" });
if (!response.ok) throw new Error("Unable to create a BeatAPI Realtime Session.");
const session = (await response.json()) as SessionResponse;

const input = await navigator.mediaDevices.getUserMedia({ video: true });
const output = document.querySelector<HTMLVideoElement>("#realtime-output");
if (!output) throw new Error("Missing #realtime-output video element.");

const client = createRealtimeClient({
clientSecret: session.data.client_secret,
});
const connection = await client.connect({
input,
output,
initial: { prompt: "Transform the scene while preserving motion." },
});

await connection.set({
prompt: "Keep the movement and change the scene to watercolor.",
});

window.addEventListener("pagehide", () => {
void connection.disconnect();
input.getTracks().forEach((track) => track.stop());
});
18 changes: 18 additions & 0 deletions examples/curl/realtime-session.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail

: "${BEATAPI_API_KEY:?Set BEATAPI_API_KEY before running this example.}"
BASE_URL="${BEATAPI_BASE_URL:-https://api.beatapi.io}"
IDEMPOTENCY_KEY="${1:-realtime-$(date +%s)}"

curl --fail-with-body --silent --show-error \
"${BASE_URL}/v1/realtime/sessions" \
-X POST \
-H "Authorization: Bearer ${BEATAPI_API_KEY}" \
-H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
-H "Content-Type: application/json" \
--data '{
"max_duration_seconds": 60,
"allowed_origins": ["https://app.example.com"],
"metadata": {"example": "curl"}
}'
24 changes: 24 additions & 0 deletions examples/node/lib/beatapi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,30 @@ export class BeatAPIClient {
});
}

createRealtimeSession(input, { idempotencyKey } = {}) {
if (!idempotencyKey) {
throw new TypeError("idempotencyKey is required.");
}
return this.request("/v1/realtime/sessions", {
method: "POST",
body: input,
headers: { "idempotency-key": idempotencyKey },
});
}

getRealtimeSession(sessionId) {
return this.request(
`/v1/realtime/sessions/${encodeURIComponent(sessionId)}`,
);
}

closeRealtimeSession(sessionId) {
return this.request(
`/v1/realtime/sessions/${encodeURIComponent(sessionId)}`,
{ method: "DELETE" },
);
}

getTask(taskId) {
return this.request(`/v1/tasks/${encodeURIComponent(taskId)}`);
}
Expand Down
33 changes: 33 additions & 0 deletions examples/node/realtime-session.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { randomUUID } from "node:crypto";

import { BeatAPIClient, BeatAPIError } from "./lib/beatapi.mjs";

const client = new BeatAPIClient();

try {
const session = await client.createRealtimeSession(
{
max_duration_seconds: 60,
allowed_origins: ["https://app.example.com"],
metadata: { example: "node" },
},
{ idempotencyKey: randomUUID() },
);

console.log(JSON.stringify({
id: session.id,
status: session.status,
expires_at: session.expires_at,
client_secret_received: Boolean(session.client_secret),
}, null, 2));
console.log("Return client_secret only to the exact allowed browser origin; do not log it.");
} catch (error) {
if (error instanceof BeatAPIError) {
console.error(
`[${error.status}] ${error.code}: ${error.message} (${error.requestId || "no request id"})`,
);
} else {
console.error(error);
}
process.exitCode = 1;
}
34 changes: 31 additions & 3 deletions examples/python/beatapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,21 @@ def request(
*,
method: str = "GET",
body: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
) -> Any:
data = json.dumps(body).encode() if body is not None else None
headers = {
request_headers = {
"Accept": "application/json",
"Authorization": f"Bearer {self.api_key}",
**(headers or {}),
}
if data is not None:
headers["Content-Type"] = "application/json"
request_headers["Content-Type"] = "application/json"

request = urllib.request.Request(
f"{self.base_url}{path}",
data=data,
headers=headers,
headers=request_headers,
method=method,
)

Expand Down Expand Up @@ -119,6 +121,32 @@ def create_ecommerce_video_task(
body=input_data,
)

def create_realtime_session(
self,
input_data: dict[str, Any],
*,
idempotency_key: str,
) -> dict[str, Any]:
if not idempotency_key:
raise ValueError("idempotency_key is required")
return self.request(
"/v1/realtime/sessions",
method="POST",
body=input_data,
headers={"Idempotency-Key": idempotency_key},
)

def get_realtime_session(self, session_id: str) -> dict[str, Any]:
encoded = urllib.parse.quote(session_id, safe="")
return self.request(f"/v1/realtime/sessions/{encoded}")

def close_realtime_session(self, session_id: str) -> dict[str, Any]:
encoded = urllib.parse.quote(session_id, safe="")
return self.request(
f"/v1/realtime/sessions/{encoded}",
method="DELETE",
)

def get_task(self, task_id: str) -> dict[str, Any]:
return self.request(f"/v1/tasks/{urllib.parse.quote(task_id, safe='')}")

Expand Down
41 changes: 41 additions & 0 deletions examples/python/realtime_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import json
import uuid

from beatapi import BeatAPIClient, BeatAPIError


def main() -> None:
client = BeatAPIClient()
session = client.create_realtime_session(
{
"max_duration_seconds": 60,
"allowed_origins": ["https://app.example.com"],
"metadata": {"example": "python"},
},
idempotency_key=str(uuid.uuid4()),
)
print(
json.dumps(
{
"id": session["id"],
"status": session["status"],
"expires_at": session.get("expires_at"),
"client_secret_received": bool(session.get("client_secret")),
},
indent=2,
)
)
print(
"Return client_secret only to the exact allowed browser origin; do not log it."
)


if __name__ == "__main__":
try:
main()
except BeatAPIError as error:
print(
f"[{error.status}] {error.code}: {error} "
f"({error.request_id or 'no request id'})"
)
raise SystemExit(1) from error
20 changes: 20 additions & 0 deletions fixtures/realtime-session-created.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"data": {
"id": "rts_01JEXAMPLE",
"object": "realtime.session",
"status": "ready",
"client_secret": "brt_live_example",
"expires_at": "2026-07-31T15:30:00.000Z",
"max_duration_seconds": 60,
"allowed_origins": ["https://app.example.com"],
"credits": {
"reserved": 60,
"settled": 0,
"refunded": 0
},
"request_id": "req_realtime_example",
"created_at": "2026-07-31T15:29:00.000Z",
"connected_at": null,
"closed_at": null
}
}
Loading