Watchlists and alerts
A watchlist is a saved set of filters. Every 30 minutes TenderScope loads new data, matches it against all active watchlists and sends alerts.
Alert kinds
Section titled “Alert kinds”reason |
When |
|---|---|
new |
A tender matching the filters appeared |
amended |
A matching tender changed (new deadline, title or documents) |
deadline-24h |
A matching open tender has its deadline within the next 24 hours |
The same alert (watchlist + tender + kind) is never sent twice.
Cadence by plan
Section titled “Cadence by plan”- Free: alerts are collected and sent as one daily digest (the first cycle after 08:00 UTC). Email only.
- Pro, Team, API: alerts go out in every 30-minute cycle, by email and/or webhook.
Failed deliveries are retried up to 5 times with growing pauses (2, 4, 8, 16 minutes). After that the alert is marked failed and not sent again.
One email per watchlist per cycle, listing every tender:
Subject: TenderScope: 2 tender alerts for "IT in Armenia"
2 tenders matched your watchlist "IT in Armenia":
1. [New] Supply of server equipment Buyer: Example Ministry (AM) Budget: not stated Deadline: 2026-11-05T08:00:00.000Z Link: https://www.armeps.am/epps/cft/prepareViewCfTWS.do?resourceId=0000000...Manage watchlists: https://tenderscope.app/appThe HTML version has the same content with links. The text above is illustrative.
Webhook
Section titled “Webhook”Set channels.webhook to an https URL (Pro and above). Plain http, localhost and private, loopback or link-local addresses are rejected. TenderScope sends one POST per watchlist per cycle with Content-Type: application/json:
{ "watchlistId": "9c1e7b0a-0000-4000-8000-000000000000", "watchlistName": "IT in Armenia", "sentAt": "2026-11-04T08:30:00.000Z", "alerts": [ { "id": "42", "reason": "deadline-24h", "createdAt": "2026-11-04T08:30:00.000Z", "tender": { "id": "1234", "source": "gnumner", "title": "Supply of server equipment", "buyerName": "Example Ministry", "buyerCountry": "AM", "budgetAmount": null, "budgetCurrency": "AMD", "deadlineAt": "2026-11-05T08:00:00.000Z", "url": "https://www.armeps.am/epps/cft/prepareViewCfTWS.do?resourceId=0000000" } } ]}Answer with any 2xx status to confirm. Anything else counts as a failure and is retried.
Verify the signature
Section titled “Verify the signature”Every request carries the header
X-TenderScope-Signature: t=1793780000,v1=3f1a…t is the Unix time in seconds, v1 is the hex HMAC-SHA256 of the string t + "." + rawBody with your signing secret. Always verify against the raw body (before JSON parsing), compare in constant time and reject old timestamps.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyTenderScopeSignature(rawBody, header, secret, toleranceSec = 300) { const parts = Object.fromEntries(header.split(',').map((p) => p.split('='))); const t = Number(parts.t); if (!Number.isFinite(t) || !parts.v1) return false; if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // replay protection
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); const given = Buffer.from(parts.v1, 'hex'); const wanted = Buffer.from(expected, 'hex'); return given.length === wanted.length && timingSafeEqual(given, wanted);}
// Express: app.post('/tenderscope', express.raw({ type: 'application/json' }), (req, res) => {// const ok = verifyTenderScopeSignature(req.body.toString('utf8'), req.get('X-TenderScope-Signature') ?? '', process.env.TENDERSCOPE_SECRET);// if (!ok) return res.sendStatus(401);// ...// res.sendStatus(204);// });Where to get your signing secret
Section titled “Where to get your signing secret”Every account has its own signing secret. Open the dashboard, find Webhook signing secret and click Show signing secret. Store it on your server (for example as TENDERSCOPE_SECRET) and pass it to verifyTenderScopeSignature above. Webhooks of all your watchlists are signed with this secret; other accounts have different secrets.