Webhooks

Receive real-time event notifications via HTTP POST. Secured with HMAC-SHA256 signatures, nonce-based replay prevention, and secret rotation.

How Webhooks Work

  1. Create a webhook in Settings → Webhooks or via the API
  2. Select which events you want to receive
  3. Save the signing secret (shown once on creation)
  4. Share.cy sends a POST request to your URL when events occur
  5. Verify the signature, timestamp, and nonce to ensure authenticity

Delivery Headers

Each webhook delivery includes these headers:

HeaderDescription
X-ShareCy-EventEvent type (e.g., post.published)
X-ShareCy-DeliveryUnique delivery UUID for idempotency
X-ShareCy-TimestampUnix timestamp when the delivery was signed
X-ShareCy-NonceUnique nonce for replay attack prevention
X-ShareCy-SignatureHMAC-SHA256 signature: sha256=XXXX
X-ShareCy-Signature-PreviousSignature 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 secret
    • X-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:

AttemptRetry After
21 minute
35 minutes
430 minutes
52 hours
624 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

EventDescription
post.createdA new post was created
post.approvedA post was approved for publishing
post.publishedA post was published to social platforms
post.failedA post failed to publish
post.deletedA post was deleted
social_account.connectedA social account was connected
social_account.disconnectedA social account was disconnected
team_member.invitedA team member was invited
team_member.acceptedA team member accepted an invitation
team_member.removedA team member was removed
subscription.updatedSubscription plan was updated
subscription.canceledSubscription was canceled
inbox.message_receivedNew 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" }