SDKs
Call the Pozy API from Node.js and TypeScript with the built-in fetch, including a small copyable wrapper for errors, idempotency, retries, uploads and pagination.
There is no Pozy package to install for Node.js. Node.js 18 and later ship with fetch, which is all the API needs. Every Node example in these docs is plain fetch.
Create an API key in the dashboard under Developers → API keys and put it in the environment:
export POZY_API_KEY="pz_live_..."#Publish your first post
This publishes a post to the first connected LinkedIn account right away.
const headers = {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
};
const listRes = await fetch("https://api.pozy.ai/v1/accounts?platform=linkedin", { headers });
const listed = await listRes.json();
if (!listRes.ok) throw new Error(listed.error);
const res = await fetch("https://api.pozy.ai/v1/posts", {
method: "POST",
headers,
body: JSON.stringify({
content: "Hello from the Pozy API",
platforms: [{ platform: "linkedin", accountId: listed.accounts[0]._id }],
publishNow: true,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.post.status); // "published", or "publishing" while it goes out
console.log(data.post.platforms?.[0]?.platformPostUrl); // link to the live post#A small wrapper
Most projects end up with one function that sends the key, encodes JSON and turns error bodies into errors. Copy this one and adjust it as you like:
export class PozyApiError extends Error {
constructor(
public status: number,
public body: { error?: string; type?: string; code?: string; param?: string; details?: unknown },
public requestId: string | null,
) {
super(body.error ?? `Pozy API error ${status}`);
}
}
type Options = { query?: Record<string, string | number | boolean | undefined>; body?: unknown; idempotencyKey?: string };
export async function pozy<T = any>(method: string, path: string, opts: Options = {}): Promise<T> {
const url = new URL(path, "https://api.pozy.ai");
for (const [k, v] of Object.entries(opts.query ?? {})) if (v !== undefined) url.searchParams.set(k, String(v));
const headers: Record<string, string> = { Authorization: `Bearer ${process.env.POZY_API_KEY}` };
if (opts.body !== undefined) headers["Content-Type"] = "application/json";
if (opts.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
const res = await fetch(url, { method, headers, body: opts.body === undefined ? undefined : JSON.stringify(opts.body) });
const text = await res.text();
const data = text ? JSON.parse(text) : null;
if (!res.ok) throw new PozyApiError(res.status, data ?? {}, res.headers.get("x-request-id"));
return data as T;
}import { pozy } from "./pozy";
const { accounts } = await pozy("GET", "/v1/accounts", { query: { profileId: "6650f1c2a8b4e3d2c1b0a998" } });
const scheduled = await pozy("GET", "/v1/posts", { query: { status: "scheduled", limit: 50 } });
await pozy("PUT", "/v1/ads/campaigns/120201234567890/status", { body: { status: "paused", platform: "facebook" } });Paths are exactly as the API reference prints them, with the path parameters filled in.
#Types
For typed requests and responses, generate types from the OpenAPI spec:
npx openapi-typescript https://pozy.ai/openapi.json -o src/pozy.d.tsThat page also shows how to use the generated types with a typed fetch client.
#Upload media
Posts take media as URLs. To post a local file, ask Pozy for an upload URL, PUT the bytes to it without an Authorization header, then use the public URL:
import { readFile, stat } from "node:fs/promises";
import { pozy } from "./pozy";
const file = "launch.mp4";
const { size } = await stat(file);
const upload = await pozy("POST", "/v1/media/presign", { body: { filename: file, contentType: "video/mp4", size } });
const put = await fetch(upload.uploadUrl, { method: "PUT", headers: { "Content-Type": "video/mp4" }, body: await readFile(file) });
if (!put.ok) throw new Error(`Upload failed with status ${put.status}`);
await pozy("POST", "/v1/posts", {
body: {
content: "Launch day.",
mediaItems: [{ type: "video", url: upload.publicUrl }],
platforms: [{ platform: "tiktok", accountId: "6650f1c2a8b4e3d2c1b0a111" }],
publishNow: true,
},
});Size and format rules per platform are in Media uploads.
#Idempotency
Send an idempotency key on writes you might retry. A retry with the same key returns the original result instead of doing the work twice.
await pozy("POST", "/v1/sms/messages", {
idempotencyKey: `sms-${order.id}`,
body: { from: "+14155550142", to: "+13125550188", text: "Your order has shipped." },
});Derive the key from your own id for the thing you are sending, and reuse it on every retry. A fresh random key per attempt defeats the purpose. See Idempotency.
#Errors
Every error answer has the same JSON body:
| Field | What it holds |
|---|---|
error |
A message written for people |
type |
The error family, such as invalid_request_error, permission_error or rate_limit_error |
code |
A stable code, such as read_only_key or profile_out_of_scope |
param |
The field that caused it, when there is one |
details |
Extra data, such as per-field validation errors |
The x-request-id response header identifies the request for support.
import { PozyApiError, pozy } from "./pozy";
try {
await pozy("POST", "/v1/posts", { body: { content: "", platforms: [] } });
} catch (err) {
if (!(err instanceof PozyApiError)) throw err; // network error, timeout, bug
if (err.status === 429) {
console.log("Rate limited. Wait and try again.");
} else if (err.body.type === "invalid_request_error") {
console.log(`Fix ${err.body.param ?? "the request"}:`, err.message, err.body.details);
} else {
console.log(`${err.status} ${err.body.code}: ${err.message} (request ${err.requestId})`);
}
}Branch on the status, type and code, never on the message text. Messages can change; codes such as read_only_key, resource_group_disabled and rate_limit_exceeded do not.
#Retry safely
Retry two kinds of failures: 429, after the wait the API asks for, and 5xx or network errors, with backoff. Do not retry other 4xx errors, because the same request fails the same way.
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export async function withRetry(send: () => Promise<Response>, attempts = 4): Promise<Response> {
for (let i = 1; ; i++) {
let res: Response | null = null;
try {
res = await send();
} catch (err) {
if (i >= attempts) throw err; // network error or timeout
}
if (res && res.status !== 429 && res.status < 500) return res;
if (res && i >= attempts) return res;
const retryAfter = Number(res?.headers.get("retry-after"));
const wait = retryAfter > 0 ? retryAfter * 1000 : Math.min(30_000, 500 * 2 ** i);
await sleep(wait + Math.random() * 250);
}
}
// Same idempotency key on every attempt, so a retry never posts twice.
const res = await withRetry(() =>
fetch("https://api.pozy.ai/v1/posts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "order-1042-thanks",
},
body: JSON.stringify(body),
}),
);#Paginate
List endpoints take page and limit, and many also take a cursor. The response tells you whether there is more:
import { pozy } from "./pozy";
let page = 1;
for (;;) {
const data = await pozy("GET", "/v1/posts", { query: { status: "published", page, limit: 100 } });
for (const post of data.posts ?? []) console.log(post._id, post.content?.slice(0, 40));
if (!data.pagination || page >= (data.pagination.pages ?? 1)) break;
page++;
}See Pagination for which endpoints use pages and which use cursors.
#Webhooks
To receive events, expose an HTTPS endpoint and check the X-Pozy-Signature header, a hex HMAC-SHA256 of the raw request body made with the endpoint secret:
import { createHmac, timingSafeEqual } from "node:crypto";
export async function POST(req: Request) {
const raw = await req.text(); // the body exactly as received, before JSON.parse
const expected = createHmac("sha256", process.env.POZY_WEBHOOK_SECRET!).update(raw).digest("hex");
const received = req.headers.get("x-pozy-signature") ?? "";
const ok = received.length === expected.length && timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!ok) return new Response("bad signature", { status: 401 });
const event = JSON.parse(raw);
console.log(req.headers.get("x-pozy-event"), event);
return new Response("ok");
}Verify signatures explains the check step by step.
#Runtime notes
- Node.js 18 or later, for the built-in
fetch. Deno, Bun and serverless runtimes havefetchtoo. - Keep it on the server. A key in browser code is a key anyone can copy.
#Related
- Authentication: key scopes, read-only keys and resource groups.
- Errors: every
typeand the commoncodevalues. - Rate limits: limits per plan and the headers to watch.
- OpenAPI spec: generate types or a client in any language.