Webhooks
Receive real-time event notifications via HTTP POST. Secured with HMAC-SHA256 signatures, nonce-based replay prevention, and secret rotation.
How Webhooks Work
- Create a webhook in Settings → Webhooks or via the API
- Select which events you want to receive
- Save the signing secret (shown once on creation)
- Share.cy sends a POST request to your URL when events occur
- Verify the signature, timestamp, and nonce to ensure authenticity
Delivery Headers
Each webhook delivery includes these headers:
| Header | Description |
|---|---|
X-ShareCy-Event | Event type (e.g., post.published) |
X-ShareCy-Delivery | Unique delivery UUID for idempotency |
X-ShareCy-Timestamp | Unix timestamp when the delivery was signed |
X-ShareCy-Nonce | Unique nonce for replay attack prevention |
X-ShareCy-Signature | HMAC-SHA256 signature: sha256=XXXX |
X-ShareCy-Signature-Previous | Signature with previous secret (during rotation grace period only) |
Payload Format
{
"event": "post.published",
"timestamp": "2026-03-21T12:00:00Z",
"data": {
"post_id": "550e8400-e29b-41d4-a716-446655440000",
"content": "Hello world!",
"platforms": ["twitter", "linkedin"],
"published_at": "2026-03-21T12:00:00Z"
}
}Verifying Signatures
The signature is computed as:
HMAC-SHA256(secret, timestamp + '.' + nonce + '.' + JSON.stringify(body))
You must verify all three components: the signature, the timestamp freshness (reject if >5 minutes old), and nonce uniqueness (prevent replays).
const crypto = require('crypto');
// Store used nonces (use Redis/DB in production)
const usedNonces = new Set();
const REPLAY_WINDOW = 300; // 5 minutes
function verifyWebhook(req, secret) {
const body = req.body; // raw string body
const signature = req.headers['x-sharecy-signature'];
const timestamp = req.headers['x-sharecy-timestamp'];
const nonce = req.headers['x-sharecy-nonce'];
// 1. Check timestamp freshness
const age = Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp));
if (age > REPLAY_WINDOW) {
throw new Error('Webhook too old or too new');
}
// 2. Check nonce uniqueness (prevent replay)
if (usedNonces.has(nonce)) {
throw new Error('Nonce already used (replay attack)');
}
usedNonces.add(nonce);
// 3. Verify HMAC signature
const signaturePayload = timestamp + '.' + nonce + '.' + body;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(signaturePayload)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature), Buffer.from(expected)
)) {
throw new Error('Invalid signature');
}
return JSON.parse(body);
}Secret Rotation
You can rotate your webhook signing secret at any time. When rotated:
- A new secret is generated and returned (shown once)
- The old secret remains valid for 1 hour (grace period)
- During the grace period, deliveries include two signature headers:
X-ShareCy-Signature— signed with the new secretX-ShareCy-Signature-Previous— signed with the old secret
- After the grace period, only the new secret is used
// Verify with support for secret rotation
function verifyWithRotation(req, newSecret, oldSecret) {
const signature = req.headers['x-sharecy-signature'];
const prevSignature = req.headers['x-sharecy-signature-previous'];
const timestamp = req.headers['x-sharecy-timestamp'];
const nonce = req.headers['x-sharecy-nonce'];
const body = req.body;
const sigPayload = timestamp + '.' + nonce + '.' + body;
// Try new secret first
const expected = 'sha256=' + crypto
.createHmac('sha256', newSecret)
.update(sigPayload).digest('hex');
if (crypto.timingSafeEqual(
Buffer.from(signature), Buffer.from(expected)
)) {
return true; // Valid with new secret
}
// Fall back to old secret during grace period
if (oldSecret) {
const oldExpected = 'sha256=' + crypto
.createHmac('sha256', oldSecret)
.update(sigPayload).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature), Buffer.from(oldExpected)
);
}
return false;
}Retry Behavior
If your endpoint returns a non-2xx response or times out (10s), we retry with exponential backoff:
| Attempt | Retry After |
|---|---|
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 24 hours |
After 10 consecutive failures, the webhook is automatically disabled and you will receive an email notification.
Testing
Use the "Send Test Event" button in Settings → Webhooks to send a test delivery. The test payload:
{
"event": "test",
"timestamp": "2026-03-21T12:00:00Z",
"data": {
"message": "This is a test webhook delivery from Share.cy",
"webhook_id": "your-webhook-uuid"
}
}Available Events
| Event | Description |
|---|---|
post.created | A new post was created |
post.approved | A post was approved for publishing |
post.published | A post was published to social platforms |
post.failed | A post failed to publish |
post.deleted | A post was deleted |
social_account.connected | A social account was connected |
social_account.disconnected | A social account was disconnected |
team_member.invited | A team member was invited |
team_member.accepted | A team member accepted an invitation |
team_member.removed | A team member was removed |
subscription.updated | Subscription plan was updated |
subscription.canceled | Subscription was canceled |
inbox.message_received | New social inbox message received |
Event Payloads
post.created{ "post_id": "uuid", "content": "...", "platforms": ["twitter"], "status": "draft" }post.approved{ "post_id": "uuid", "approved_by": "uuid" }post.published{ "post_id": "uuid", "content": "...", "platforms": ["twitter", "linkedin"], "published_at": "2026-..." }post.failed{ "post_id": "uuid", "error": "Rate limited by platform", "platform": "twitter" }post.deleted{ "post_id": "uuid" }social_account.connected{ "platform": "twitter", "username": "@example", "account_id": "uuid" }social_account.disconnected{ "platform": "twitter", "reason": "token_expired", "account_id": "uuid" }team_member.invited{ "email": "user@example.com", "role": "editor" }team_member.accepted{ "user_id": "uuid", "email": "user@example.com", "role": "editor" }team_member.removed{ "user_id": "uuid", "email": "user@example.com" }subscription.updated{ "plan": "pro", "status": "active", "period_end": "2026-..." }subscription.canceled{ "plan": "pro", "cancel_at": "2026-..." }inbox.message_received{ "platform": "twitter", "from": "@user", "content": "...", "message_id": "uuid" }