Skip to main content
Webhooks let your application react to changes in a user’s Bkmark account the moment they happen — no polling required. When an event occurs (a bookmark is created, a tag is renamed, a group is deleted), Bkmark sends an HTTP POST request containing a JSON payload to the URL you registered. You verify the request came from Bkmark using an HMAC-SHA256 signature, then process the event however your application needs.

Creating a Subscription

Send a POST request to /api/v1/webhooks with the endpoint URL you want to receive events and the list of events you want to subscribe to.
curl -X POST https://api.bkmark.it/api/v1/webhooks \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/bkmark",
    "events": ["bookmark.created", "bookmark.deleted", "tag.created"]
  }'
The secret field is returned only once — at creation time. Store it immediately in a secure secrets manager (such as AWS Secrets Manager, HashiCorp Vault, or an environment variable). You cannot retrieve it again. If you lose it, delete the subscription and create a new one.
Response 201 Created:
{
  "id": "wh_01hq8p2z3kxv7m4n9rbt5c6d",
  "url": "https://yourapp.com/webhooks/bkmark",
  "secret": "whsec_a3f8c2d1e94b76052718af3c0d9e1b47",
  "events": ["bookmark.created", "bookmark.deleted", "tag.created"],
  "active": true,
  "createdAt": "2025-06-01T09:00:00.000Z"
}
id
string
Unique identifier for this webhook subscription. Use it to update, delete, or query deliveries.
url
string
The HTTPS endpoint Bkmark will POST events to.
secret
string
HMAC-SHA256 signing secret. Shown once only. Use this to verify the X-Bkmark-Signature header on every incoming request.
events
array
The event types this subscription will receive.
active
boolean
Whether the subscription is currently enabled.

Request Body Parameters

url
string
required
The HTTPS endpoint Bkmark will deliver events to. Must be publicly accessible.
events
array of strings
required
One or more event type strings. Subscribe to all events with ["*"], or list individual events. See Available Events below.

Available Events

Bkmark emits events across three resource types: bookmarks, groups, and tags.
EventTrigger
bookmark.createdA new bookmark is saved
bookmark.updatedA bookmark’s title, description, or other fields change
bookmark.deletedA bookmark is permanently deleted
bookmark.favoritedA bookmark is marked as a favorite (transition only)
bookmark.unfavoritedA bookmark’s favorite status is removed (transition only)
bookmark.archivedA bookmark is archived (transition only)
bookmark.unarchivedA bookmark is unarchived (transition only)
bookmark.restoredA soft-deleted bookmark is restored from trash
group.createdA new group is created
group.updatedA group’s name or color changes
group.deletedA group is deleted
tag.createdA new tag is created
tag.updatedA tag’s name or color changes
tag.deletedA tag is deleted
Transition events (bookmark.favorited, bookmark.unfavorited, bookmark.archived, bookmark.unarchived) fire only when the boolean state actually changes. For example, PATCHing isArchived: true on a bookmark that is already archived does not fire bookmark.archived. No-op updates to tags and groups also do not fire tag.updated or group.updated.

Payload Format

Every webhook delivery is a POST request with Content-Type: application/json. The top-level structure is consistent across all events:
{
  "event": "bookmark.created",
  "timestamp": "2025-06-01T14:32:10.000Z",
  "data": { }
}
event
string
The event type string, e.g. bookmark.created.
timestamp
string (ISO 8601)
UTC timestamp of when the event occurred. Use this along with the entity id for idempotency checks.
data
object
The event payload. Shape varies by event type — see the examples below.

Payload Examples


Signature Verification

Every delivery includes an X-Bkmark-Signature header containing the HMAC-SHA256 hex digest of the raw request body, computed using your webhook secret.
Always verify the signature before processing any webhook. Skip this step and an attacker can send forged events to your endpoint. Use a timing-safe comparison function — standard string equality is vulnerable to timing attacks.
The verification algorithm is:
  1. Read the raw request body as bytes (do not parse JSON first).
  2. Compute HMAC-SHA256(secret, rawBody) and encode as a hex string.
  3. Compare the result with the value in X-Bkmark-Signature using a timing-safe equality function.
  4. Reject any request where the values do not match.

Node.js

import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const app = express();

// Use express.raw() so you get the unmodified body buffer
app.post('/webhooks/bkmark', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-bkmark-signature'];
  const secret = process.env.BKMARK_WEBHOOK_SECRET;

  if (!signature) {
    return res.status(401).json({ error: 'Missing signature' });
  }

  const expected = createHmac('sha256', secret)
    .update(req.body) // req.body is a Buffer when using express.raw()
    .digest('hex');

  const sigBuffer = Buffer.from(signature);
  const expBuffer = Buffer.from(expected);

  // Reject if lengths differ (timingSafeEqual requires equal-length buffers)
  if (sigBuffer.length !== expBuffer.length) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  if (!timingSafeEqual(sigBuffer, expBuffer)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Signature is valid — parse and process the event
  const { event, timestamp, data } = JSON.parse(req.body.toString());

  switch (event) {
    case 'bookmark.created':
      // e.g. sync to your own database
      console.log('New bookmark:', data.bookmark.url);
      break;
    case 'bookmark.deleted':
      console.log('Deleted bookmark ID:', data.bookmarkId);
      break;
    // Handle other events...
  }

  // Respond quickly — queue heavy work for a background job
  res.status(200).send('OK');
});

Python

import hmac
import hashlib
import os
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["BKMARK_WEBHOOK_SECRET"]


def verify_signature(raw_body: bytes, signature: str) -> bool:
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    # hmac.compare_digest is timing-safe
    return hmac.compare_digest(signature, expected)


@app.post("/webhooks/bkmark")
def handle_webhook():
    signature = request.headers.get("X-Bkmark-Signature", "")
    raw_body = request.get_data()  # raw bytes, before any JSON parsing

    if not verify_signature(raw_body, signature):
        abort(401, "Invalid signature")

    payload = request.get_json()
    event = payload["event"]

    if event == "bookmark.created":
        bookmark = payload["data"]["bookmark"]
        print(f"New bookmark: {bookmark['url']}")
    elif event == "bookmark.deleted":
        print(f"Deleted bookmark ID: {payload['data']['bookmarkId']}")
    # Handle other events...

    return "", 200

Managing Subscriptions

Use the following endpoints to list, update, delete, and test your webhook subscriptions.
MethodPathDescription
GET/api/v1/webhooksList all subscriptions for the authenticated user
PATCH/api/v1/webhooks/:idUpdate a subscription’s URL, event list, or active status
DELETE/api/v1/webhooks/:idPermanently delete a subscription
POST/api/v1/webhooks/:id/testSend a test event to verify your endpoint is reachable
GET/api/v1/webhooks/:id/deliveriesView the last 50 delivery attempts and their HTTP response codes
Example — disable a subscription without deleting it:
curl -X PATCH https://api.bkmark.it/api/v1/webhooks/wh_01hq8p2z3kxv7m4n9rbt5c6d \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "active": false }'

Delivery and Retries

Bkmark dispatches webhook requests asynchronously from a background job queue. The following guarantees and constraints apply:
  • Your endpoint must return a 2xx status code within 10 seconds. Any non-2xx response or a timeout is treated as a failure.
  • Failed deliveries are retried 3 times using exponential backoff.
  • You can inspect every attempt — including the HTTP response code your server returned — via GET /api/v1/webhooks/:id/deliveries.
Respond with 200 OK immediately and offload any heavy processing to a background worker or task queue. This prevents timeouts caused by slow database writes or third-party API calls inside your handler.
Because deliveries can be retried, your handler should be idempotent. Use the timestamp field and the entity id inside data together as a deduplication key — if you’ve already processed an event with that combination, skip it.

Polling Helpers

If you need to backfill events or reconcile state without webhooks, you can poll the bookmarks list endpoint with time-based filters.
ParameterDescription
?since=<ISO 8601>Return bookmarks with createdAt after the given timestamp
?updatedSince=<ISO 8601>Return bookmarks with updatedAt after the given timestamp
Both filters are strictly greater-than — a bookmark whose timestamp exactly matches the boundary is excluded. Combine with sortField=updatedAt&sortDir=desc for efficient incremental sync.
# Fetch all bookmarks created after June 1st
curl "https://api.bkmark.it/api/v1/bookmarks?since=2025-06-01T00:00:00.000Z&sortField=createdAt&sortDir=desc" \
  -H "Authorization: Bearer ACCESS_TOKEN"

# Fetch all bookmarks updated in the last 15 minutes
curl "https://api.bkmark.it/api/v1/bookmarks?updatedSince=2025-06-01T14:15:00.000Z&sortField=updatedAt&sortDir=desc" \
  -H "Authorization: Bearer ACCESS_TOKEN"