dibs.

Webhooks

Dibs can send every alert as an HTTPS POST to a URL you own, signed so you can tell it came from us. Add an endpoint under Alerts in your settings. Webhooks are on every plan, including Free.

Adding an endpoint

The URL must use https:// and resolve to a public address. When you add it we send one test delivery (a listing titled “Test delivery from Dibs”); your endpoint must answer it with a 2xx. Then we show your signing secret, once. It starts with whsec_. We never show it again: if you lose it, remove the endpoint and add it again to get a new one. Each endpoint has its own secret.

The test delivery is signed with the new secret before you have seen it, so accept it without verifying, or verify it once you have copied the secret.

The request

One listing per request:

POST <your URL>
content-type: application/json
user-agent: dibs-webhooks/1 (+https://dibs.deals)
dibs-event: listing.found
dibs-signature: t=1760647800,v1=5f2c8e…

{
  "v": 1,
  "type": "listing.found",
  "sentAt": "2026-10-16T20:50:00.000Z",
  "listing": {
    "id": "7c1e2f4a-3b5d-4e6f-8a9b-0c1d2e3f4a5b",
    "title": "Herman Miller Aeron chair, size B",
    "url": "https://www.facebook.com/marketplace/item/1234567890/",
    "priceRaw": "$450",
    "priceCents": 45000,
    "location": "Austin, TX",
    "imageUrl": "https://dibs.deals/i/7c1e2f4a-3b5d-4e6f-8a9b-0c1d2e3f4a5b",
    "source": "facebook-marketplace",
    "searchId": "2d4f6a8c-1e3b-4d5f-9a7c-8b6e4d2f0a1c",
    "searchLabel": "Office chairs",
    "listedAt": "2026-10-16T20:41:00.000Z",
    "firstSeenAt": "2026-10-16T20:49:12.000Z",
    "priceDrop": null
  }
}

dibs-event is listing.found for a new listing or listing.price_drop when a listing you were already sent drops in price. It always matches type in the body.

Payload

The body is { v, type, sentAt, listing }. v is 1. We only ever add fields to version 1, so ignore any you don’t recognise; a breaking change would come as v: 2.

listing.TypeMeaning
idstringThe listing id. Stable for the life of the listing.
titlestringThe listing title, as the marketplace shows it.
urlstringLink to the listing.
priceRawstring | nullThe price as the marketplace wrote it.
priceCentsnumber | nullThe price in cents, when it parses. For a price drop, the new price.
locationstring | nullWhere the listing says it is.
imageUrlstring | nullThe first photo.
sourcestringfacebook-marketplace, craigslist or offerup.
searchIdstringThe saved search that found it.
searchLabelstring | nullThat search’s name.
listedAtstring | nullWhen the seller listed it (ISO 8601), when known.
firstSeenAtstringWhen Dibs first saw it (ISO 8601).
priceDropobject | nullSet on listing.price_drop: fromCents, toCents, dropCents, dropPercent, since.
startingSampleboolean (optional)True when the listing was already up when the search started watching.

Verifying the signature

The dibs-signature header is t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256, hex-encoded, of the string t, a period, and the raw request body, keyed with your full secret including the whsec_ prefix. It is the same scheme Stripe uses.

  1. Read the raw body before parsing the JSON. Re-serialising it changes the bytes and the signature won’t match.
  2. Split the header on , and read t and v1.
  3. Compute HMAC-SHA256(secret, t + "." + rawBody) as hex and compare it to v1 in constant time.
  4. Reject the request if t is more than about five minutes from your clock. The timestamp is signed, so this stops replays.

Node

const crypto = require('node:crypto')

// rawBody: the request body as a Buffer or string, before JSON.parse.
function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)))
  const expected = crypto
    .createHmac('sha256', secret)
    .update(parts.t + '.')
    .update(rawBody)
    .digest('hex')
  const a = Buffer.from(expected)
  const b = Buffer.from(parts.v1 ?? '')
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= toleranceSeconds
  return a.length === b.length && crypto.timingSafeEqual(a, b) && fresh
}

Python

import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, sig = parts["t"], parts["v1"]
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig) and abs(time.time() - int(t)) <= tolerance

Responding and retries

Answer with any 2xx within 10 seconds. Redirects are not followed and count as a failure, so give us the final URL. Anything else, or no answer in time, is a failure: we retry after a minute, then wait twice as long each time, up to an hour between tries. After 8 failures in a row we turn the endpoint off and show the last error in your settings, where you can turn it back on.

A delivery can arrive more than once, for example if your endpoint did the work but answered after the timeout. Use listing.id with type to skip ones you have already handled.

Help

Email support@dibs.deals.