Webhooks
A webhook is an address on your server. Postra sends it a message each time a post goes out, so your other tools can react: post a note in Slack, add a row to a client report or update your CRM.
When Postra calls it
There is one event: a post was published.
- Postra calls your webhook right after a post is published on a channel.
- A post sent to three channels means three calls, one per channel.
- Drafts, scheduled posts, failed posts and deleted posts do not trigger a call.
Add a webhook
You need to be the workspace owner or an Admin.
- In Postra, open Settings and then the Webhooks tab.
- Click Add a webhook.
- Fill in Name (for you) and URL (your HTTPS address).
- In Integrations, choose All integrations or Specific integrations. With specific integrations, pick the channels.
- Click Save.
- Click Edit on the new webhook. Under Signing secret, click Reveal and copy the secret to your server.
Send Test posts an example message to the URL. On a saved webhook (opened with Edit), the test is signed like a real delivery, so you can try your signature check with it.
To change the name, URL or channels, click Edit. To stop deliveries, click Delete.
Which channels it covers
| Setting | Gets calls for |
|---|---|
| All integrations | Every channel in the workspace, including channels you connect later |
| Specific integrations | Only the channels you picked |
What Postra sends
A POST request with a JSON body. The body is an array. A real delivery holds one post: the one just published. Send Test sends two example posts.
| Field | Meaning |
|---|---|
id |
The post’s ID in Postra |
content |
The post text. It can contain simple HTML tags such as <p>. |
publishDate |
The time the post was set to go out, ISO 8601 in UTC |
releaseURL |
Link to the live post on the platform |
state |
PUBLISHED |
integration.id |
The channel’s ID in Postra |
integration.name |
The channel’s name |
integration.providerIdentifier |
The platform, for example facebook, instagram or linkedin |
integration.picture |
The channel’s picture |
integration.type |
The kind of channel, for example social |
Example:
[
{
"id": "POST_ID",
"content": "<p>Our autumn sale starts today.</p>",
"publishDate": "2026-10-08T09:30:00.000Z",
"releaseURL": "https://www.facebook.com/YOUR_PAGE/posts/POST_ON_FACEBOOK",
"state": "PUBLISHED",
"integration": {
"id": "CHANNEL_ID",
"name": "Your Shop",
"providerIdentifier": "facebook",
"picture": "https://example.com/your-page-picture.png",
"type": "social"
}
}
]
The message holds post details only. It never holds passwords or channel tokens.
Headers
| Header | Value |
|---|---|
Content-Type |
application/json |
Postra-Webhook-Id |
Which of your webhooks sent it. Useful when one URL gets several webhooks. |
Postra-Signature |
t=<time>,v1=<signature> |
Check the signature
Each webhook has its own signing secret. It starts with whsec_. Only the workspace owner and Admins can see it.
How the signature is made:
tis the time Postra sent the request, in Unix seconds.v1is an HMAC-SHA256, in hex, of the text<t>.<raw body>, made with your signing secret.
On your server:
- Read the raw body exactly as it arrived, before any JSON parsing.
- Work out the same HMAC with your secret and compare it with
v1. Use a constant-time compare. - Refuse requests where
tis more than 5 minutes away from now. This stops someone replaying an old message.
Node.js example with Express:
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const SECRET = process.env.POSTRA_WEBHOOK_SECRET; // YOUR_WEBHOOK_SECRET
function isFromPostra(rawBody, header, secret, toleranceSeconds = 300) {
if (!header) return false;
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)));
const t = Number(parts.t);
if (!parts.v1 || !Number.isInteger(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1, 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}
const app = express();
app.post('/postra-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8');
if (!isFromPostra(rawBody, req.get('Postra-Signature'), SECRET)) {
return res.status(400).send('Bad signature');
}
const posts = JSON.parse(rawBody);
for (const post of posts) {
console.log(post.integration.name, post.releaseURL);
}
res.sendStatus(200);
});
app.listen(3000);
New secret
If the secret leaks, open the webhook with Edit and click Generate a new secret. The old secret stops working at once. Update your server straight away, or it will reject the next deliveries.
Timeouts and retries
- Postra waits up to 5 seconds for your answer.
- Any 2xx or 4xx answer, except 429, counts as delivered.
- A 429, a 5xx, a timeout, a dropped connection or a redirect counts as failed. Postra does not follow redirects.
- After a failure Postra tries again: after 2 seconds, then after 4 seconds. That is 3 attempts in total. After that the message is dropped.
- Each attempt carries a fresh
tand signature.
Answer quickly and do slow work later. If your server is slow, it can get the same post twice. Use the post id to skip ones you already handled.
Limits per plan
| Plan | Webhooks |
|---|---|
| Starter | 2 |
| Pro | 30 |
| Business | 10,000 |
The Webhooks tab shows how many you use, for example Webhooks (1/2).
Paused webhooks
A webhook is paused when the workspace has more webhooks than its plan allows, for example after moving to a smaller plan.
- The oldest webhooks keep working, up to the plan limit.
- The newer ones show Paused in the list and get no calls.
- To start them again, upgrade or delete older webhooks.
Addresses that are refused
The URL must be a public https:// address. Postra refuses:
http://addresseslocalhost- private, internal, loopback and link-local addresses, such as
10.x.x.x,192.168.x.xor127.0.0.1 - names that point to any of these
Postra checks this when you save the webhook, when you send a test and again before each delivery.