Webhooks
Receive an HTTPS POST from Pozy when a post publishes, a message arrives or an account disconnects. Create an endpoint, verify it, and handle your first event.
Webhooks tell your app when something happens instead of making you ask. Pozy sends an HTTPS POST with a JSON body to your URL each time an event you subscribed to occurs: a post published, a DM arrived, a review was left, an account needs reconnecting. This page takes you from nothing to a verified endpoint receiving events.
#How it works
- You run an HTTPS endpoint that accepts
POSTrequests. - You register it with Pozy, with a secret and the events you want.
- Pozy signs each delivery with your secret and sends it. Your endpoint checks the signature, stores the event and answers
2xxwithin 5 seconds. - If it does not answer
2xxin time, Pozy tries again, up to 7 attempts in total. See Retries.
#Build a receiver
Your handler needs the raw request body to verify the signature, so read the bytes before any JSON parsing.
// npm install express
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.POZY_WEBHOOK_SECRET!;
const queue: { id?: string; type: string; payload: unknown }[] = []; // stand-in for your job queue
app.post("/webhooks/pozy", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.get("X-Pozy-Signature") ?? "";
const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const valid = signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid) return res.status(401).send("bad signature");
const event = JSON.parse(req.body.toString("utf8"));
// Store it and hand it to a background job; answer quickly.
queue.push({ id: req.get("X-Pozy-Event-Id"), type: event.event, payload: event });
res.sendStatus(200);
});
app.listen(3001);# pip install flask
import hashlib
import hmac
import json
import os
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["POZY_WEBHOOK_SECRET"].encode()
def enqueue(event_id, event_type, payload):
... # stand-in for your job queue
@app.post("/webhooks/pozy")
def pozy_webhook():
raw = request.get_data() # bytes, before any parsing
expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Pozy-Signature", "")):
return "bad signature", 401
event = json.loads(raw)
# Store it and hand it to a background job; answer quickly.
enqueue(request.headers["X-Pozy-Event-Id"], event["event"], event)
return "", 200The Signatures page covers the check in detail, and Idempotency and ordering shows how to make the background job safe to run twice.
#Register the endpoint
Generate a long random secret, keep it in your server's environment, and create the endpoint with POST /v1/webhooks/settings:
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": "Production",
"url": "https://app.example.com/webhooks/pozy",
"secret": "'"$POZY_WEBHOOK_SECRET"'",
"events": ["post.published", "post.partial", "post.failed", "account.disconnected", "message.received"]
}'const res = 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: "Production",
url: "https://app.example.com/webhooks/pozy",
secret: process.env.POZY_WEBHOOK_SECRET,
events: ["post.published", "post.partial", "post.failed", "account.disconnected", "message.received"],
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.webhook._id);import os
import requests
resp = requests.post(
"https://api.pozy.ai/v1/webhooks/settings",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
json={
"name": "Production",
"url": "https://app.example.com/webhooks/pozy",
"secret": os.environ["POZY_WEBHOOK_SECRET"],
"events": ["post.published", "post.partial", "post.failed", "account.disconnected", "message.received"],
},
timeout=30,
)
print(resp.json()["webhook"]["_id"]){
"success": true,
"webhook": {
"_id": "whk_4Tq9Lm2Vx8Rb6Nc1Pz7K",
"name": "Production",
"url": "https://app.example.com/webhooks/pozy",
"events": ["post.published", "post.partial", "post.failed", "account.disconnected", "message.received"],
"isActive": true,
"profileIds": [],
"accountIds": [],
"customHeaders": {},
"failureCount": 0,
"lastFiredAt": null
}
}Pozy signs deliveries with the secret you choose and never generates one for you. An endpoint without a secret receives unsigned requests that anyone who knows the URL could forge.
Then send a test event to check the whole path: POST /v1/webhooks/test with the webhookId. See Test webhooks.
#Endpoint settings
| Field | Required | What it does |
|---|---|---|
name |
Yes | A label for you, 1 to 50 characters. |
url |
Yes | Where deliveries go. Use HTTPS in production. Redirects are not followed, so give the final URL. |
events |
Yes | At least one event name. Unknown names are rejected with code: "invalid_event". See the events reference. |
secret |
No, but set it | The key for the X-Pozy-Signature HMAC. |
profileIds |
No | Only send events that belong to these profiles. Empty means every profile. |
accountIds |
No | Only send events about these connected accounts. Empty means every account. |
customHeaders |
No | Extra headers added to every delivery, for example a static token your gateway expects: { "X-Gateway-Token": "..." }. |
isActive |
No | false pauses deliveries without deleting the endpoint. Defaults to true. |
A workspace can have up to 50 endpoints. Use several when different services care about different events, for example one for publishing results and one for the inbox.
#Send events to the right place
By default an endpoint receives matching events for the whole workspace. Two filters narrow it:
profileIdskeeps events whose profile is in the list. A post event matches when any account it targets belongs to one of those profiles.accountIdskeeps events about one of those connected accounts.
With both set, an event must match both. Events that belong to no profile (workspace-level events, for example) only reach endpoints with no profileIds filter.
A common use is staging: put your test accounts in their own profile and point a staging endpoint at it with profileIds. Prefer profile filters to account filters, because a profile id stays the same when its account is reconnected.
If you run a platform with one profile per customer, every event also carries profiles[].externalId, your own customer id, so one endpoint can route to every tenant without a lookup. See Route webhooks to tenants.
#What a delivery looks like
POST /webhooks/pozy HTTP/1.1
Host: app.example.com
Content-Type: application/json
User-Agent: Pozy-Webhooks/1.0
X-Pozy-Event: post.published
X-Pozy-Event-Id: 8e0b6f8a-3c51-4b7e-9d0a-2f4c6e1b9a77
X-Pozy-Delivery-Id: dlv_Qm3Zt8Kp1Wx6Bn4Rc9Ly
X-Pozy-Attempt: 1
X-Pozy-Signature: 3f6c1d0b9a7e5c2f4d8b6a1e0c9f7d5b3a2e4c6f8d0b1a3c5e7f9d2b4a6c8e0f{
"id": "8e0b6f8a-3c51-4b7e-9d0a-2f4c6e1b9a77",
"event": "post.published",
"timestamp": "2026-11-03T08:30:04.000Z",
"profiles": [{ "id": "670f9b1d4c2e8a7f3b6d5e10", "externalId": "cust_4821" }],
"post": {
"id": "6721a8c04e9b2f1d7c3a5e88",
"content": "Scheduled exports are live.",
"status": "published",
"scheduledFor": "2026-11-03T08:30:00.000Z",
"publishedAt": "2026-11-03T08:30:03.000Z",
"platforms": [
{
"platform": "linkedin",
"status": "published",
"accountId": "6710c3e2a9f14b0d8e5c7a21",
"platformPostId": "urn:li:share:7259012345678901234",
"publishedUrl": "https://www.linkedin.com/feed/update/urn:li:share:7259012345678901234"
}
],
"metadata": { "campaignId": "cmp_2041" }
}
}| Header | Meaning |
|---|---|
X-Pozy-Event |
The event name, same as event in the body. |
X-Pozy-Event-Id |
The event's id, same as id in the body. Identical on every retry and redelivery, so use it to deduplicate. |
X-Pozy-Delivery-Id |
This delivery. A redelivery of the same event gets a new delivery id. |
X-Pozy-Attempt |
Which attempt this is, from 1 to 7. |
X-Pozy-Signature |
Hex HMAC-SHA256 of the raw body, keyed with your secret. Sent when the endpoint has a secret. |
Every body has id, event and timestamp. Events that belong to a profile add profiles, and the rest of the body depends on the event: post, account, message, comment and so on. Test deliveries add "test": true.
#Answer fast
Pozy waits 5 seconds for a response. Do the minimum inside the request: check the signature, write the event to a table or queue, answer 200. Do the real work (updating your database, calling other services, sending email) in a background job. An endpoint that does slow work inline will time out, get retried, and process the same event several times.
Any 2xx status counts as delivered. The body of your response is ignored, but the first 2,000 characters are kept in the delivery logs to help you debug.
#Manage endpoints
| Task | Call |
|---|---|
| List endpoints, with their secrets and failure counts | GET /v1/webhooks/settings |
| Change events, URL, filters or secret | PUT /v1/webhooks/settings with webhookId and the fields to change |
| Pause or resume | PUT /v1/webhooks/settings with isActive |
| Delete | DELETE /v1/webhooks/settings?webhookId=whk_... |
Managing endpoints needs a workspace owner or admin in the dashboard, or an API key that has not disabled the webhooks resource group.
#Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/webhooks/settings |
List webhooks |
POST /v1/webhooks/settings |
Create webhook |
PUT /v1/webhooks/settings |
Update webhook |
DELETE /v1/webhooks/settings |
Delete webhook |
GET /v1/webhooks/logs |
List webhook delivery logs |
POST /v1/webhooks/logs/redeliver |
Redeliver a webhook event |
POST /v1/webhooks/test |
Send test webhook |