API and webhooks
Put tender data where your team already works
A versioned REST API for matched opportunities, readiness and bid status, and signed webhooks when something changes. Available on Pro and Enterprise plans.
Keys that belong to your organisation
- Keys are owned by the organisation, not a person, so they survive staff changes.
- Each key has scopes such as
opportunities:readorbids:read. - Shown once, stored hashed, revocable instantly, with per-key rate limits.
- Every call appears in your organisation's activity log.
curl https://app.example.com/api/public/v1/opportunities?closingWithinDays=14 \
-H "Authorization: Bearer tnd_live_…"The OpenAPI 3.1 reference is generated from the same schemas the API validates with, and lists each endpoint as it goes live: openapi.json
Webhooks you can trust
Every delivery is signed with HMAC-SHA256 and a timestamp, carries a stable event ID for de-duplication, and is retried over 24 hours if your endpoint is down.
import { createHmac, timingSafeEqual } from "node:crypto";
// header: Tender-Signature: t=1759140000,v1=5f2c…
export function verify(rawBody: string, header: string, secret: string) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // replay window
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(v1, "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}Events
| Event | Sent when |
|---|---|
| opportunity.matched | A new tender matches your organisation |
| tender.changed | A tender you follow changed materially: deadline, value, documents, clarifications |
| tender.deadline_approaching | A followed tender closes within your chosen window |
| readiness.changed | Your readiness score or blockers changed |
| bid.status_changed | A bid moved state, for example to approved or submitted |
| evidence.expiring | A document a bid depends on is about to expire |