Getting started
Create an API key, connect an account, publish and schedule a post, and receive your first signed webhook in about ten minutes.
This page takes you from an empty workspace to a published post and a verified webhook. You need a Pozy account and a login for one social platform. The examples use LinkedIn, but every OAuth platform works the same way.
Your first two connected accounts are free and need no card. X is the exception: it bills every API call, so connecting an X account asks for a card first (pricing).
#Create an API key
Open API keys in the dashboard and click Create key. Give it a name you will recognise later, such as quickstart, and keep the defaults (full access, read and write).
Copy the key straight away. Pozy stores only a hash of it, so it is shown once. Keys start with pz_live_.
Put it in an environment variable. Every example below reads it from there.
export POZY_API_KEY="pz_live_..."The Node examples use the fetch built into Node.js 18 and later, and the Python examples use requests. There is nothing Pozy-specific to install.
#Check the key
GET /v1/auth/verify tells you whether a credential works and what it can reach. It is the cheapest call to make when something looks wrong.
curl https://api.pozy.ai/v1/auth/verify \
-H "Authorization: Bearer $POZY_API_KEY"const res = await fetch("https://api.pozy.ai/v1/auth/verify", {
headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data);import os, requests
resp = requests.get(
"https://api.pozy.ai/v1/auth/verify",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
)
print(resp.json()){
"valid": true,
"userId": "usr_4fQ9kT2mZx8LwB1nR7cY",
"name": "Alex Rivera",
"email": "alex@example.com",
"authType": "api_key",
"scope": "full"
}A 401 with "code": "invalid_api_key" means the key was mistyped, revoked or expired.
#Create a profile
A profile is a container for connected accounts: one per brand, client or customer of yours. Create one for this walkthrough and keep its _id.
curl -X POST https://api.pozy.ai/v1/profiles \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Quickstart" }'const createdRes = await fetch("https://api.pozy.ai/v1/profiles", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Quickstart" }),
});
const created = await createdRes.json();
if (!createdRes.ok) throw new Error(created.error);
const profileId = created.profile._id;resp = requests.post(
"https://api.pozy.ai/v1/profiles",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
json={"name": "Quickstart"},
)
profile_id = resp.json()["profile"]["_id"]{
"message": "Profile created successfully",
"profile": {
"_id": "6720f3b1c9e4a85d2f0b7c14",
"name": "Quickstart",
"externalId": null,
"isDefault": false,
"accountCount": 0
}
}#Connect an account
Ask for a connect URL for the platform and the profile. Open the authUrl in a browser, sign in to LinkedIn and approve.
curl "https://api.pozy.ai/v1/connect/linkedin?profileId=6720f3b1c9e4a85d2f0b7c14" \
-H "Authorization: Bearer $POZY_API_KEY"const connectRes = await fetch(`https://api.pozy.ai/v1/connect/linkedin?${new URLSearchParams({ profileId })}`, {
headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const connect = await connectRes.json();
if (!connectRes.ok) throw new Error(connect.error);
console.log("Open this URL and approve:", connect.authUrl);resp = requests.get(
"https://api.pozy.ai/v1/connect/linkedin",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
params={"profileId": profile_id},
)
print("Open this URL and approve:", resp.json()["authUrl"])Pozy hosts every screen of the flow, including the page that asks whether to connect your personal LinkedIn profile or a company page. When you pass a redirect_url, the browser comes back to it with the new account's accountId in the query string. Without one, the flow ends on a Pozy page that says the account is connected.
Connecting accounts covers branded connect pages, headless flows for your own UI and the platforms that use credentials instead of OAuth.
#Find the account id
Posts target accounts by their _id, never by username. List the accounts in the profile:
curl "https://api.pozy.ai/v1/accounts?profileId=6720f3b1c9e4a85d2f0b7c14" \
-H "Authorization: Bearer $POZY_API_KEY"const listedRes = await fetch(`https://api.pozy.ai/v1/accounts?${new URLSearchParams({ profileId })}`, {
headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const listed = await listedRes.json();
if (!listedRes.ok) throw new Error(listed.error);
const accountId = listed.accounts[0]._id;resp = requests.get(
"https://api.pozy.ai/v1/accounts",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
params={"profileId": profile_id},
)
account_id = resp.json()["accounts"][0]["_id"]{
"accounts": [
{
"_id": "6720f41a8d3be96c1e5a02f7",
"platform": "linkedin",
"profileId": "6720f3b1c9e4a85d2f0b7c14",
"username": "alex-rivera",
"displayName": "Alex Rivera",
"isActive": true
}
],
"hasAnalyticsAccess": true
}#Publish a post now
Send the text, the target account and publishNow: true. The Idempotency-Key header makes the call safe to retry: if the network drops and you send it again with the same key, you get the same post back instead of a second one.
curl -X POST https://api.pozy.ai/v1/posts \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-first-post" \
-d '{
"content": "Hello from the Pozy API.",
"platforms": [{ "platform": "linkedin", "accountId": "6720f41a8d3be96c1e5a02f7" }],
"publishNow": true
}'const publishedRes = await fetch("https://api.pozy.ai/v1/posts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Idempotency-Key": "quickstart-first-post",
"Content-Type": "application/json",
},
body: JSON.stringify({
content: "Hello from the Pozy API.",
platforms: [{ platform: "linkedin", accountId }],
publishNow: true,
}),
});
const published = await publishedRes.json();
if (!publishedRes.ok) throw new Error(published.error);
console.log(published.post.status, published.post.platforms[0].platformPostUrl);resp = requests.post(
"https://api.pozy.ai/v1/posts",
headers={
"Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
"Idempotency-Key": "quickstart-first-post",
},
json={
"content": "Hello from the Pozy API.",
"platforms": [{"platform": "linkedin", "accountId": account_id}],
"publishNow": True,
},
)
post = resp.json()["post"]
print(post["status"], post["platforms"][0].get("platformPostUrl"))The post comes back with an overall status and one entry per target in platforms, each with its own status, platformPostId and platformPostUrl once it is live.
#Schedule a post
Leave out publishNow and send scheduledFor instead. Pair it with timezone so the time is read the way you wrote it:
curl -X POST https://api.pozy.ai/v1/posts \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-scheduled-post" \
-d '{
"content": "This one goes out tomorrow morning.",
"platforms": [{ "platform": "linkedin", "accountId": "6720f41a8d3be96c1e5a02f7" }],
"scheduledFor": "2026-11-02T09:00:00",
"timezone": "Europe/Madrid"
}'const scheduledRes = await fetch("https://api.pozy.ai/v1/posts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Idempotency-Key": "quickstart-scheduled-post",
"Content-Type": "application/json",
},
body: JSON.stringify({
content: "This one goes out tomorrow morning.",
platforms: [{ platform: "linkedin", accountId }],
scheduledFor: "2026-11-02T09:00:00",
timezone: "Europe/Madrid",
}),
});
const scheduled = await scheduledRes.json();
if (!scheduledRes.ok) throw new Error(scheduled.error);
console.log(scheduled.post._id, scheduled.post.status); // "scheduled"resp = requests.post(
"https://api.pozy.ai/v1/posts",
headers={
"Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
"Idempotency-Key": "quickstart-scheduled-post",
},
json={
"content": "This one goes out tomorrow morning.",
"platforms": [{"platform": "linkedin", "accountId": account_id}],
"scheduledFor": "2026-11-02T09:00:00",
"timezone": "Europe/Madrid",
},
)
print(resp.json()["post"]["status"]) # "scheduled"Until it publishes, you can change or cancel it with PUT /v1/posts/{postId} and DELETE /v1/posts/{postId}. Scheduling and timezones has the details.
#Receive a webhook
Instead of polling the post, let Pozy tell you when it goes live. You need a public HTTPS URL. For local testing, run a tunnel to your machine and use its URL.
This small Node server accepts deliveries and checks the signature:
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.POZY_WEBHOOK_SECRET;
createServer((req, res) => {
let raw = "";
req.on("data", (chunk) => (raw += chunk));
req.on("end", () => {
const expected = createHmac("sha256", SECRET).update(raw).digest("hex");
const got = req.headers["x-pozy-signature"] ?? "";
const ok = got.length === expected.length && timingSafeEqual(Buffer.from(got), Buffer.from(expected));
if (!ok) { res.writeHead(401).end(); return; }
const event = JSON.parse(raw);
console.log(req.headers["x-pozy-event"], event.id);
res.writeHead(200).end(); // answer within 5 seconds
});
}).listen(3000);Pick a signing secret, start the server with it, then register the endpoint for the post events:
export POZY_WEBHOOK_SECRET="$(openssl rand -hex 32)"
curl -X POST https://api.pozy.ai/v1/webhooks/settings \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Quickstart",
"url": "https://your-tunnel.example.com/",
"secret": "'"$POZY_WEBHOOK_SECRET"'",
"events": ["post.published", "post.failed", "post.partial"]
}'const hookRes = await fetch("https://api.pozy.ai/v1/webhooks/settings", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Quickstart",
url: "https://your-tunnel.example.com/",
secret: process.env.POZY_WEBHOOK_SECRET,
events: ["post.published", "post.failed", "post.partial"],
}),
});
const hook = await hookRes.json();
if (!hookRes.ok) throw new Error(hook.error);
console.log(hook.webhook._id);resp = requests.post(
"https://api.pozy.ai/v1/webhooks/settings",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
json={
"name": "Quickstart",
"url": "https://your-tunnel.example.com/",
"secret": os.environ["POZY_WEBHOOK_SECRET"],
"events": ["post.published", "post.failed", "post.partial"],
},
)
print(resp.json()["webhook"]["_id"])Send a test delivery to check the wiring before the real post goes out:
curl -X POST https://api.pozy.ai/v1/webhooks/test \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "webhookId": "whk_8Jq2LmT4vX9cR1sZp0Ya3Kd", "event": "post.published" }'When the scheduled post publishes, your server prints post.published and the event id. The body looks like this:
{
"id": "evt_8a1f4c2e9b7d3a60",
"event": "post.published",
"timestamp": "2026-11-02T08:00:03.412Z",
"profiles": [{ "id": "6720f3b1c9e4a85d2f0b7c14", "externalId": null }],
"post": {
"id": "6721a0c47b2e5f9d3a18c6e0",
"content": "This one goes out tomorrow morning.",
"status": "published",
"scheduledFor": "2026-11-02T08:00:00.000Z",
"publishedAt": "2026-11-02T08:00:03.120Z",
"platforms": [
{
"platform": "linkedin",
"status": "published",
"accountId": "6720f41a8d3be96c1e5a02f7",
"publishedUrl": "https://www.linkedin.com/feed/update/urn:li:share:7255..."
}
]
}
}Endpoints created without a secret receive unsigned deliveries. Set one and reject any request whose X-Pozy-Signature does not match. Verify signatures shows the check in Node and Python.