Webhooks
SchedPilot sends an HTTP POST request to a URL of your choice whenever a post event occurs. Webhooks let your application react to publish outcomes in real time without polling the API.
Setup
- Log in to app.schedpilot.com and go to API Access.
- Find the Webhooks section.
- Enter your endpoint URL (must be
https://). - Click Save.
- Copy your signing secret — you will need it to verify incoming requests.
SchedPilot only delivers webhooks to https:// endpoints. Plain http:// URLs are rejected at save time.
Events
| Event | Triggered when |
|---|---|
post.published | A scheduled post was successfully published to the social platform. |
post.failed | A publish attempt was made but the platform rejected it or an error occurred. |
Payload Structure
All webhook requests share the same Content-Type: application/json header. The event field indicates which event occurred.
post.published
{
"event": "post.published",
"post_id": 1024,
"content": "Hello world! Check out our latest update.",
"platform": "twitter",
"account_id": 7,
"platform_post_id": "1801234567890123456",
"scheduled_date": "2024-06-15 09:00:00",
"published_at": "2024-06-15T09:00:05Z"
}
| Field | Type | Description |
|---|---|---|
event | string | Always "post.published". |
post_id | integer | SchedPilot internal post ID. |
content | string | The text content of the post. |
platform | string | The social platform (e.g. "twitter", "linkedin", "instagram"). |
account_id | integer | The connected account that published the post. |
platform_post_id | string | The native post ID assigned by the social platform. |
scheduled_date | string | The original scheduled date/time (UTC). |
published_at | string | ISO 8601 UTC timestamp of when the post was actually published. |
post.failed
{
"event": "post.failed",
"post_id": 1025,
"content": "New product launch tomorrow!",
"platform": "instagram",
"account_id": 12,
"scheduled_date": "2024-06-15 10:30:00",
"error_message": "The access token has expired. Please reconnect your Instagram account."
}
| Field | Type | Description |
|---|---|---|
event | string | Always "post.failed". |
post_id | integer | SchedPilot internal post ID. |
content | string | The text content of the post. |
platform | string | The social platform that rejected the publish attempt. |
account_id | integer | The connected account that was used. |
scheduled_date | string | The original scheduled date/time (UTC). |
error_message | string | Human-readable description of what went wrong. |
Signature Verification
Every webhook request includes an X-SchedPilot-Signature header. The value is the HMAC-SHA256 of the raw request body, computed using your signing secret as the key, and hex-encoded.
Always verify this signature before processing the payload. Reject any request where verification fails — it was not sent by SchedPilot.
Go to app.schedpilot.com > API Access > Webhooks. Your signing secret is displayed below your endpoint URL.
Verification in Node.js
const crypto = require('crypto');
function verifySchedPilotSignature(rawBody, signatureHeader, signingSecret) {
const expected = crypto
.createHmac('sha256', signingSecret)
.update(rawBody)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
const expectedBuf = Buffer.from(expected, 'hex');
const receivedBuf = Buffer.from(signatureHeader, 'hex');
if (expectedBuf.length !== receivedBuf.length) {
return false;
}
return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}
// Express example — important: use express.raw() so the body is a Buffer
const express = require('express');
const app = express();
app.post('/webhooks/schedpilot', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-schedpilot-signature'];
const signingSecret = process.env.SCHEDPILOT_SIGNING_SECRET;
if (!verifySchedPilotSignature(req.body, signature, signingSecret)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
if (event.event === 'post.published') {
console.log(`Post ${event.post_id} published to ${event.platform}`);
// Handle successful publish...
} else if (event.event === 'post.failed') {
console.error(`Post ${event.post_id} failed: ${event.error_message}`);
// Notify your team, trigger a retry flow, etc.
}
res.sendStatus(200);
});
Verification in PHP
<?php
function verifySchedPilotSignature(string $rawBody, string $signatureHeader, string $signingSecret): bool {
$expected = hash_hmac('sha256', $rawBody, $signingSecret);
return hash_equals($expected, $signatureHeader);
}
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SCHEDPILOT_SIGNATURE'] ?? '';
$signingSecret = getenv('SCHEDPILOT_SIGNING_SECRET');
if (!verifySchedPilotSignature($rawBody, $signature, $signingSecret)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($rawBody, true);
if ($event['event'] === 'post.published') {
// Handle successful publish
error_log("Post {$event['post_id']} published to {$event['platform']}");
} elseif ($event['event'] === 'post.failed') {
// Handle failure
error_log("Post {$event['post_id']} failed: {$event['error_message']}");
}
http_response_code(200);
Responding to Webhook Requests
- Your endpoint must return an HTTP
200status code within 5 seconds. - Any non-
2xxresponse (or a timeout) is considered a delivery failure. - Retries are not currently implemented. If your endpoint is down when an event fires, the delivery will be lost. Plan accordingly — for example, log all incoming events immediately before doing any heavy processing.
To stay within the 5-second window, acknowledge the request right away and hand off the actual work to a background job or queue.