# Canadian Government Tender Monitor (`jonmcconnell/canadian-government-tender-monitor`) Actor

Track Government of Canada tender opportunities matched to your business. Buyer contacts, closing dates, GSIN/UNSPSC filtering and relevance scoring, from official open data. Not affiliated with the Government of Canada.

- **URL**: https://apify.com/jonmcconnell/canadian-government-tender-monitor.md
- **Developed by:** [Jon Mcconnell](https://apify.com/jonmcconnell) (community)
- **Categories:** Lead generation, Agents, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 tender notices

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Canadian Government Tender Monitor

**Find Government of Canada contracts your business can actually bid on.** Reads every federal tender notice from official open data, scores each against what you do, and returns clean rows with buyer contacts, closing dates, and eligibility details.

Run once for a snapshot, or **schedule it daily** to get new opportunities the morning they're published.

*Not affiliated with, or endorsed by, the Government of Canada.*

### Quick start

Click **Run**. The defaults return currently open federal tenders matching software and consulting work.

To tune it for your business, set two things:

- **Keywords** — what you deliver (`janitorial`, `engineering`, `translation`)
- **GSIN or UNSPSC code prefixes** — Canada's classification codes. Prefixes work, so `D3` catches all D3xx categories

Everything else is optional.

### What you get

One row per notice:

| Field | Description |
|---|---|
| `title`, `description` | What the government wants |
| `organization` | The buying department or agency |
| `contactName`, `contactEmail`, `contactPhone` | Who to talk to |
| `closingDate`, `daysUntilClosing`, `urgency` | How long you have |
| `publicationDate`, `amendmentNumber`, `amendmentDate` | Notice history |
| `tradeAgreements` | Which trade agreements apply — **determines whether you're eligible to bid at all** |
| `procurementMethod`, `selectionCriteria` | How the bid will be run and judged |
| `limitedTenderingReason` | Why competition was limited, where applicable |
| `procurementCategories` | Construction / Goods / Services |
| `gsinCodes`, `unspscCodes` | Official classification codes |
| `buyerProvince`, `buyerCity`, `postalCode` | Where the buying department is |
| `regionsOfDelivery`, `regionsOfOpportunity` | Where the work is |
| `expectedStartDate`, `expectedEndDate` | Contract period |
| `solicitationNumber`, `referenceNumber` | IDs for cross-referencing award data |
| `relevanceScore`, `matchedKeywords` | Why this matched you, 0–100 |
| `searchUrl` | Opens CanadaBuys with this notice's solicitation number |
| `noticeUrl` | Direct link, where the government provides one |
| `language` | Whether the source notice was English or French |

### How relevance scoring works

Deterministic and explainable — no AI, no randomness, identical output for identical input.

- Keyword in the **title**: +40
- Keyword in the **description**: +15
- **GSIN or UNSPSC** code match: +35
- Capped at 100

The minimum score defaults to **1**, meaning a notice must match *something* to be returned. Set it to `0` to browse everything regardless of match, or raise it toward `40` once your keywords are tuned. If you set no keywords or codes at all, every notice scores 100 and the threshold has no effect.

### Recommended setup: a daily opportunity feed

1. Set **Which notices** to `Published today only`
2. Set your keywords and codes
3. Set **Minimum relevance score** to `40`
4. Schedule the Actor each morning
5. Pipe the dataset to Slack, email, Google Sheets, or Zapier via Apify integrations

New matching federal contracts arrive daily without you checking a portal.

### Two data quirks this Actor handles for you

**Notices marked "Open" that closed months ago.** The source data leaves the status field stale — testing found notices still marked Open more than a year past their closing date. **Hide notices past their closing date** is on by default, so you only see things you can still bid on. Turn it off for historical research.

**Missing links.** Only some notices carry a direct URL in the open data, and CanadaBuys' internal page slugs can't be derived from any published field. Rather than construct links that 404, every row gets a `searchUrl` that opens CanadaBuys pre-filtered to that solicitation number. It always resolves.

### Bilingual by design

Federal notices are published in English and French, and some records populate only one side. This Actor returns the English text where available and **falls back to French rather than returning a blank** — so you don't silently lose opportunities. The `language` field tells you which you got.

### Procurement categories

| Code | Meaning |
|---|---|
| `CNST` | Construction |
| `GD` | Goods |
| `SRV` | Services |
| `SRVTGD` | Services related to goods |

A notice can carry more than one.

### Coverage and limits

- **Federal notices only.** The CanadaBuys website also lists provincial and municipal opportunities, but the open data files cover federal organizations only. This Actor reflects that.
- Notices published through PSPC's SAP Ariba system appear the following day, after the overnight sync.
- Not every notice populates every field; missing values come through as `null` rather than causing a failure.
- One solicitation can appear multiple times as it's amended. Results are deduplicated per run, keeping the first occurrence.

### A note on the contact emails

The contact details are procurement officers' work addresses, published by the government so that bidders can reach them about specific solicitations.

Canada's Anti-Spam Legislation (**CASL**) governs commercial electronic messages. Contacting an officer about the specific opportunity they posted is the intended use. Adding these addresses to a bulk marketing list is not, and may expose you to penalties. Use them for what they're for.

### Data source and licensing

Data comes from the [CanadaBuys tender notices](https://open.canada.ca/data/en/dataset/6abd20d4-7a1c-4b38-baa2-9525d0bb2fd2) dataset published by Public Services and Procurement Canada, used under the [Open Government Licence – Canada](https://open.canada.ca/en/open-government-licence-canada).

The "published today" file is refreshed **every 2 hours** from 6:15 am to 10:15 pm (UTC−05:00). The "currently open" file refreshes each morning between 7:00 and 8:30 am.

This is an **official licensed open data feed**, not scraping. No proxies, no anti-bot measures, no terms-of-service grey area, and nothing that breaks when a website is redesigned.

This Actor is an independent product. It is not affiliated with, endorsed by, or sponsored by the Government of Canada or Public Services and Procurement Canada.

### Troubleshooting

**"No matching notices found"** — a filter problem, not a fault. Switch **Which notices** to `All currently open`, clear your keywords, set minimum relevance to 0, then narrow back down.

**Nothing matches my code** — try a shorter prefix. `81111500` is narrow; `8111` is broad; `81` is broader still.

**I expected provincial contracts** — see Coverage above. The federal open data files don't include them.

**Fewer results than I expected** — expired notices are hidden by default. Turn off **Hide notices past their closing date** to include them.

# Actor input Schema

## `feed` (type: `string`):

"All currently open" gives every tender accepting bids right now — best for a first look. "Published today" is the incremental feed, refreshed every 2 hours — best for a scheduled daily run.

## `keywords` (type: `array`):

Words describing what you deliver, e.g. 'software', 'janitorial', 'engineering'. A match in the title scores higher than one in the description. Leave empty to get everything that passes your other filters.

## `codes` (type: `array`):

Canada classifies tenders with GSIN (e.g. D302) and UNSPSC (e.g. 81111500) codes. Prefixes work: 'D3' catches all D3xx categories, '8111' all 8111xxxx. Browse codes at canadabuys.canada.ca.

## `categories` (type: `array`):

Restrict to broad categories. Leave empty for all.

## `regionContains` (type: `string`):

Only return notices whose delivery region, opportunity region, or buying department's own province/city mentions this text, e.g. 'Ontario', 'Alberta', 'Ottawa'. Case-insensitive.

## `openOnly` (type: `boolean`):

Exclude notices the government has marked cancelled or expired. Recognises both English and French status wording.

## `excludeExpired` (type: `boolean`):

The source data marks some notices "Open" long after their closing date has passed — occasionally by more than a year. Leave this on to see only opportunities you can still bid on. Turn it off for historical research.

## `excludeAmendments` (type: `boolean`):

Skip notices that have been amended at least once. Useful if you only want brand-new opportunities.

## `minRelevanceScore` (type: `integer`):

How well a notice must match your keywords or codes to be returned. Leave at 1 to return only genuine matches. Set to 0 to return everything regardless of match (useful for browsing). Raise toward 40 once your keywords are tuned. If you set no keywords or codes at all, every notice scores 100 and this has no effect.

## `maxResults` (type: `integer`):

Hard cap on notices returned. Also caps what you are charged, since you are only charged for notices actually delivered.

## Actor input object example

```json
{
  "feed": "open",
  "keywords": [
    "software",
    "consulting"
  ],
  "codes": [],
  "categories": [],
  "openOnly": true,
  "excludeExpired": true,
  "excludeAmendments": false,
  "minRelevanceScore": 1,
  "maxResults": 200
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "keywords": [
        "software",
        "consulting"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("jonmcconnell/canadian-government-tender-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "keywords": [
        "software",
        "consulting",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("jonmcconnell/canadian-government-tender-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "keywords": [
    "software",
    "consulting"
  ]
}' |
apify call jonmcconnell/canadian-government-tender-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jonmcconnell/canadian-government-tender-monitor"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/omJBoO6NZSa0QtKwq/builds/JFRdEAHQuttyN5UHh/openapi.json
