API Reference
PostClaw does not yet offer a public REST API for general-purpose management of posts, platforms, and automations. There are no API keys and no per-user API authentication today.
However, PostClaw supports outbound webhooks for Business tier and above — you can receive post.published events on your server whenever a post is published.
For publishing content to your own server, see Custom Blog Integration.
Outbound Webhooks
Receive POST requests from PostClaw whenever a post is published. Each webhook includes an HMAC signature for integrity verification.
Webhook Management
Webhooks are managed in the PostClaw UI under Settings → Webhooks. Creating or editing a webhook requires Business tier. Viewing your existing webhooks, reading their delivery log, retrying a failed delivery, and deleting them work at any tier — you always retain the ability to inspect, recover, and turn off what you already created. The API paths are session-authenticated (no API keys):
| Method | Path | Purpose | Tier |
|---|---|---|---|
| GET | /v1/social/webhooks | List webhooks | Any |
| POST | /v1/social/webhooks | Create webhook | Business+ |
| PUT | /v1/social/webhooks/{id} | Update webhook | Business+ |
| DELETE | /v1/social/webhooks/{id} | Delete webhook | Any |
| POST | /v1/social/webhooks/{id}/rotate-secret | Rotate signing secret | Business+ |
| GET | /v1/social/webhooks/{id}/deliveries | View delivery log | Any |
| POST | /v1/social/webhooks/{id}/deliveries/{delivery_id}/retry | Retry failed delivery | Any |
Authentication: Session cookie (sign in to PostClaw, then use the UI or authenticated requests from your server).
Delivery stops below Business tier. Unlike the read/retry/delete actions above, actual event delivery requires your account to be on Business tier or higher at the moment a post publishes — this is a property of the account, not of any single API call. If your account drops below Business, PostClaw stops sending new events to your webhooks and automatically marks them disabled so the UI reflects reality; they are not deleted, and you can still view their delivery log or delete them. Upgrading back to Business does not resume delivery automatically — re-enable the webhook (or create a new one) from Settings → Webhooks after upgrading.
Creating a Webhook
POST /v1/social/webhooks
{
"url": "https://your-server.com/postclaw/webhooks",
"events": ["post.published"],
"enabled": true
}
Response (201 Created):
{
"webhook": {
"id": "wh_abc123...",
"url": "https://your-server.com/postclaw/webhooks",
"events": ["post.published"],
"secret": "whsec_abcdef1234567890...",
"enabled": true,
"created_at": "2026-04-01T09:00:00Z"
}
}
Note: The full secret is returned only once at creation. On subsequent reads (list, get), the secret is masked as ••••abcd (last 4 chars visible).
Webhook Events
PostClaw currently sends only one event type: post.published.
Event: post.published
Fired: When a post is published to at least one platform.
Payload:
{
"event": "post.published",
"post_id": "post_abc123",
"owner_id": "owner_xyz789",
"published_at": "2026-04-01T09:00:05Z",
"targets": [
{
"platform": "twitter",
"platform_post_id": "1234567890",
"url": "https://twitter.com/user/status/1234567890"
},
{
"platform": "custom_blog",
"platform_post_id": "post_xyz",
"url": "https://your-blog.com/posts/post_xyz"
}
]
}
The targets array includes only successfully published targets (status = “published”). Targets with status “draft” or “failed” are omitted.
Delivery Headers
Every webhook delivery includes these headers, in addition to Content-Type: application/json:
| Header | Example | Purpose |
|---|---|---|
X-PostClaw-Event | post.published | The event type (see Webhook Events). |
X-PostClaw-Delivery-Id | 3f1c2a9e-... | The delivery’s unique id. Stable across retries of the same delivery — use it to dedupe. |
X-PostClaw-Timestamp | 1745136005 | Unix seconds when this specific attempt was sent. Recomputed fresh on every retry. |
X-PostClaw-Signature | sha256=<hex> | HMAC-SHA256 over timestamp.delivery_id.body (see below). |
Deduplicating Retries
A failed delivery is retried up to twice (3 attempts total, see Retry Policy) with the same payload. All 3 attempts carry the same X-PostClaw-Delivery-Id because they are the same underlying delivery — PostClaw never re-enqueues a new row for a retry. If your endpoint has already processed a given X-PostClaw-Delivery-Id, treat a repeat as a no-op rather than reprocessing it. This matters even on a fully successful first attempt: a network blip between your server accepting the request and PostClaw receiving the response still triggers a retry of the identical delivery.
HMAC Signature Verification
Every webhook delivery includes an X-PostClaw-Signature header. Use it to verify the request came from PostClaw and was not replayed.
X-PostClaw-Signature: sha256=<hex>
The signature is HMAC-SHA256, computed with the webhook secret as the key, over the exact string:
<timestamp>.<delivery_id>.<raw body>
That is: the literal value of the X-PostClaw-Timestamp header, a period, the literal value of the X-PostClaw-Delivery-Id header, another period, then the raw request body bytes — three fields joined by two literal . characters, in that exact order, with no extra whitespace anywhere. Reconstruct this string from the three header/body values you received before hashing; do not re-serialize the body or reformat the timestamp/id.
This is an extension of the scheme Stripe uses for its webhooks (Stripe signs timestamp.body; PostClaw additionally binds the delivery id). Signing the timestamp is what makes the tolerance-window check below possible: a request captured off the wire and replayed later produces the same body but an old timestamp, so a receiver checking both the signature and the timestamp’s age rejects it. Signing the delivery id closes a narrower gap on top of that: without it, an attacker who captured one valid (timestamp, signature, body) tuple could, within the tolerance window, swap in a different X-PostClaw-Delivery-Id value and the signature would still verify — defeating a receiver’s delivery-id-based dedupe without forging anything cryptographic. Binding all three fields means changing any one of them invalidates the signature.
import crypto from 'crypto';
const secret = 'whsec_abc123...'; // From webhook creation
const bodyBytes = req.rawBody; // Raw body bytes, not re-parsed JSON
const timestamp = req.headers['x-postclaw-timestamp'];
const deliveryId = req.headers['x-postclaw-delivery-id'];
const received = req.headers['x-postclaw-signature'];
// 1. Reject stale/replayed requests BEFORE checking the signature. 5 minutes
// is a generous tolerance for normal clock drift and delivery latency.
const TOLERANCE_SECONDS = 5 * 60;
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!timestamp || Number.isNaN(age) || age > TOLERANCE_SECONDS) {
return res.status(401).json({ error: 'Timestamp missing or outside tolerance window' });
}
// 2. Verify the signature over "<timestamp>.<delivery_id>.<raw body>", not
// the body alone and not just "<timestamp>.<raw body>". deliveryId must
// be present — an absent id can't be part of a valid signed string.
if (!deliveryId) {
return res.status(401).json({ error: 'Delivery id missing' });
}
const signedPayload = `${timestamp}.${deliveryId}.${bodyBytes}`;
const computed = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Use timing-safe comparison. Check the header is present and the same
// length as `computed` BEFORE calling timingSafeEqual — it throws a
// RangeError (not a boolean false) on mismatched buffer lengths, which would
// otherwise crash your handler with a 500 instead of returning a clean 401
// on a missing or malformed signature.
const computedBuf = Buffer.from(computed);
const receivedBuf = Buffer.from(received || '');
const validSignature =
computedBuf.length === receivedBuf.length && crypto.timingSafeEqual(computedBuf, receivedBuf);
if (!validSignature) {
return res.status(401).json({ error: 'Signature mismatch' });
}
// 3. Dedupe on X-PostClaw-Delivery-Id (see "Deduplicating Retries" above)
// before acting on the payload, so a legitimate retry after a network
// blip on your end doesn't double-process the event.
Retry Policy
A delivery is successful when your server returns any status below 400. Any status of 400 or above — and any network, TLS, or timeout error — counts as a failure. PostClaw does not distinguish 4xx from 5xx: both are retried the same way.
A failed delivery is retried at most twice, for 3 attempts total:
| Attempt | Sent after |
|---|---|
| 1 | Immediately |
| 2 | ~10 seconds after attempt 1 fails |
| 3 | ~60 seconds after attempt 2 fails |
After the third failure the delivery is marked permanently failed and is not retried automatically again.
Delays are approximate: a background worker scans for due deliveries every 15 seconds, so a retry fires at the first scan after its backoff elapses.
You can manually retry failed deliveries in the UI or via POST /v1/social/webhooks/{id}/deliveries/{delivery_id}/retry.
Webhook Security
- HTTPS only: Your webhook endpoint must be HTTPS; HTTP is rejected.
- Public IP: Your domain must resolve to a public IP; private IPs (10.x, 192.168.x, 172.16-31.x) and localhost are rejected.
- HMAC verification: Always verify the
X-PostClaw-Signatureheader before processing the payload — overtimestamp.delivery_id.body, not the body alone (see HMAC Signature Verification). - Reject stale timestamps: Check
X-PostClaw-Timestampis within a tolerance window (5 minutes recommended) before trusting a signature, to reject replayed requests. - Dedupe on delivery id: Use
X-PostClaw-Delivery-Idto recognize a retry of a delivery you already processed (see Deduplicating Retries). - Timeout: PostClaw times out after 10 seconds. Respond quickly or return a 202 Accepted and process asynchronously.
- Signature secret: Never expose the webhook secret in logs or error messages. The secret is customer-controlled and should be treated as sensitive.
Future API Plans
PostClaw plans to add a public REST API for general-purpose programmatic access (posts, platforms, media, automations) in a future release. Check the Changelog for announcements.