# Quickstart

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](https://pozy.ai/docs/billing)).

### Create an API key

Open [API keys](https://pozy.ai/dashboard/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.

```bash Terminal
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.

```bash cURL
curl https://api.pozy.ai/v1/auth/verify \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
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);
```
```python Python
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())
```

```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`.

```bash cURL
curl -X POST https://api.pozy.ai/v1/profiles \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Quickstart" }'
```
```ts Node
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;
```
```python Python
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"]
```

```json
{
  "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.

```bash cURL
curl "https://api.pozy.ai/v1/connect/linkedin?profileId=6720f3b1c9e4a85d2f0b7c14" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
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);
```
```python Python
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](https://pozy.ai/docs/connect) 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:

```bash cURL
curl "https://api.pozy.ai/v1/accounts?profileId=6720f3b1c9e4a85d2f0b7c14" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
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;
```
```python Python
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"]
```

```json
{
  "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.

```bash cURL
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
  }'
```
```ts Node
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);
```
```python Python
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:

```bash cURL
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"
  }'
```
```ts Node
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"
```
```python Python
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](https://pozy.ai/docs/publishing/scheduling) 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:

```ts
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:

```bash cURL
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"]
  }'
```
```ts Node
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);
```
```python Python
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:

```bash Terminal
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:

```json
{
  "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..."
      }
    ]
  }
}
```

> **Always set a secret:**
> 
> Endpoints created without a `secret` receive unsigned deliveries. Set one and reject any request whose `X-Pozy-Signature` does not match. [Verify signatures](https://pozy.ai/docs/webhooks/signatures) shows the check in Node and Python.

## Where to go next

- [Core concepts](https://pozy.ai/docs/getting-started/concepts): How workspaces, profiles, accounts and posts relate.
- [Create a post](https://pozy.ai/docs/publishing): Media, per-platform text, threads, drafts and approvals.
- [Build a platform](https://pozy.ai/docs/build): One profile per customer, scoped keys and webhook routing.
- [Errors](https://pozy.ai/docs/getting-started/errors): The error envelope and what to retry.
