Webhook Integration

Receive real-time notifications when your bookmarks or labels change. Configure a webhook URL, choose events, and verify signed payloads for security.

Setup

  1. Go to Integrations → Webhook in your TweetsMash account.
  2. Click “Create Webhook”. Enter your endpoint URL (HTTPS recommended).
  3. Select the events you want to subscribe to (see the list below).
  4. Save. A secret token will be generated and shown once — store it securely. You will use it to verify signatures.

Subscribable Events

Bookmarks Imported
bookmarks.imported
Bookmark Marked as Read
bookmarks.read
Bookmark Marked as Unread
bookmarks.unread
Bookmark Archived
bookmarks.archived
Bookmark Unarchived
bookmarks.unarchived
Bookmark Deleted
bookmarks.deleted
Label Added to Bookmark
bookmarks.labels.added
Label Removed from Bookmark
bookmarks.labels.deleted
Schedule Triggered
schedules.triggered

Delivery & Security

We deliver a JSON payload via HTTP POST to your URL. Each request is signed with an HMAC-SHA256 signature using your webhook secret.

POST /your/webhook/url HTTP/1.1
Content-Type: application/json
User-Agent: TweetsMash-Webhook/1.0
X-Webhook-Signature: sha256=<hex-digest>
X-Webhook-Timestamp: 2025-08-08T15:20:13.011Z

Verify Signature

Compute HMAC-SHA256 over the raw request body with your secret, then compare to the value inX-Webhook-Signature (format: sha256=<hex>). Optionally reject ifX-Webhook-Timestamp is older than 5 minutes.

Node.js
import crypto from 'crypto';

function timingSafeEqual(a, b) {
  const aBuf = Buffer.from(a);
  const bBuf = Buffer.from(b);
  if (aBuf.length !== bBuf.length) return false;
  return crypto.timingSafeEqual(aBuf, bBuf);
}

export function verifyTweetsMashSignature(rawBody, headers, secret) {
  const sigHeader = headers['x-webhook-signature'] || headers['X-Webhook-Signature'];
  if (!sigHeader || !sigHeader.startsWith('sha256=')) return false;
  const received = sigHeader.slice('sha256='.length);
  const computed = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');

  // Optional timestamp freshness check
  const ts = headers['x-webhook-timestamp'] || headers['X-Webhook-Timestamp'];
  if (ts) {
    const ageMs = Math.abs(Date.now() - Date.parse(ts));
    if (ageMs > 5 * 60 * 1000) return false;
  }
  return timingSafeEqual(computed, received);
}
Python
import hmac, hashlib, time

def verify_tweetsmash_signature(raw_body: bytes, headers: dict, secret: str) -> bool:
    sig_header = headers.get('x-webhook-signature') or headers.get('X-Webhook-Signature')
    if not sig_header or not sig_header.startswith('sha256='):
        return False
    received = sig_header[len('sha256='):]
    computed = hmac.new(secret.encode('utf-8'), raw_body, hashlib.sha256).hexdigest()

    ts = headers.get('x-webhook-timestamp') or headers.get('X-Webhook-Timestamp')
    if ts:
        try:
            # Basic freshness check (5 minutes)
            from datetime import datetime, timezone
            age = abs((datetime.now(timezone.utc) - datetime.fromisoformat(ts.replace('Z', '+00:00'))).total_seconds())
            if age > 300:
                return False
        except Exception:
            return False

    return hmac.compare_digest(computed, received)

Payload Structure

All events share a common envelope. data varies by event, and event_idslists identifiers of the affected tweets or labels.

{
  "id": "012cf7ea-35ec-47e3-a368-6018c2a22199",
  "event": "bookmarks.unread",
  "event_ids": [
    "1869409110774763597"
  ],
  "timestamp": "2025-08-08T15:20:13.011Z",
  "data": [
    {
      "post_id": "1869409110774763597"
    }
  ],
  "webhook_id": "8607439c-1270-42d9-a9d8-907dd91b8b10"
}

Example Payloads

{
  "id": "2c7bbc6a-34f7-49c9-a8b0-782036c1b989",
  "event": "bookmarks.imported",
  "event_ids": [
    "1953471271414714463",
    "1953398302742872144",
    "1953597493649736020"
  ],
  "timestamp": "2025-08-08T15:29:20.341Z",
  "data": [
    {
      "post_id": "1953471271414714463",
      "tweet_details": {
        "text": "As a product designer, I love cool design effects.\n\nSo I analyzed @Apple's 3D design revolution.\n\nMy #1 takeaway: everyone is copying them, but 99% of designers still don't get why it works.\n\nHere's how subtle 3D effects are changing UI (with examples that'll blow your mind): ",
        "link": "https://twitter.com/DenisJeliazkov/status/1953471271414714463",
        "posted_at": "Thu Aug 07 15:00:19 +0000 2025",
        "attachments": [
          {
            "type": "video",
            "thumbnail": "https://pbs.twimg.com/amplify_video_thumb/1953471221200551936/img/VT0RoOFjHGQFrUDp.jpg",
            "variants": []
          }
        ],
        "quoted_tweet": null,
        "meta": {
          "tweet_reply_to_tweet_id": null,
          "tweet_reply_to_user_id": null,
          "tweet_conversation_id": "1953471271414714463",
          "tweet_is_thread": false,
          "is_sub_tweet_of_thread": false,
          "non_real_time_engagement_metrics": {
            "reply_count": 17,
            "favorite_count": 407,
            "retweet_count": 20,
            "quote_count": 2,
            "bookmark_count": 432
          }
        }
      },
      "author_id": "711232574245498880",
      "author_username": "DenisJeliazkov",
      "author_details": {
        "user": {
          "author_id": "711232574245498880",
          "name": "Denislav Jeliazkov",
          "profile_image": "https://pbs.twimg.com/profile_images/1762834036169285632/OCd2-vw-_normal.jpg",
          "username": "DenisJeliazkov",
          "description": "Designer & maker \n✦ I help founders build products people love \n✦ Built 60+ products for high-growth startups. \nSee work → https://t.co/TgoHSKsCPN | Book a call ↓"
        }
      },
      "posted_at": "Thu Aug 07 15:00:19 +0000 2025",
      "sort_index": "1839901608806183041"
    }
  ],
  "webhook_id": "8607439c-1270-42d9-a9d8-907dd91b8b10"
}