# Health Canada Medical Device Licence Monitor (`automa-flow/health-canada-medical-device-licence-monitor`) Actor

Monitor Health Canada MDALL medical device licences and exact product identifiers. Track status, risk class, company and device changes with persistent baselines, batch checks and JSON/CSV exports.

- **URL**: https://apify.com/automa-flow/health-canada-medical-device-licence-monitor.md
- **Developed by:** [Vadim Bezrukov](https://apify.com/automa-flow) (community)
- **Categories:** Business, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$15.00 / 1,000 conclusive target checks

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/actors/running/actors-in-store.md#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

## Health Canada Medical Device Licence Monitor

Monitor Canadian medical device licences and exact product identifiers against
Health Canada's Medical Devices Active Licence Listing (MDALL). Keep a watchlist
for procurement or regulatory review, then receive structured changes to licence
status, risk class, company and optional device membership.

The first run returns a baseline. Repeat the same watchlist to detect changes
without building your own comparison database. Export observations through Apify
as JSON, CSV or Excel, or connect them to your existing review workflow.
Schedule recurring medical device license checks for an existing procurement watchlist.

Health Canada explains that licences can be suspended, cancelled or discontinued
and recommends verification whenever a purchase is considered. This monitor is
for known Class II, III and IV licences and labelled identifiers. It does not
search for suppliers, assess device safety or cover Class I, MDEL, recalls,
investigational testing or Special Access.

### Quickstart

Start with a saved example and replace its sample values with your own:

- [Check Health Canada Medical Device Licence Status](https://apify.com/automa-flow/health-canada-medical-device-licence-monitor/examples/check-health-canada-medical-device-licence-status) — current licence status and company details.
- [Monitor Canadian Medical Device Licence Changes](https://apify.com/automa-flow/health-canada-medical-device-licence-monitor/examples/monitor-canadian-medical-device-licence-changes) — licence and device membership changes between checks.
- [Vérifier les identifiants d'instruments médicaux au Canada](https://apify.com/automa-flow/health-canada-medical-device-licence-monitor/examples/verifier-identifiants-instruments-medicaux-canada) — a French example for exact product-label identifiers.

```json
{
  "monitorKey": "procurement-canada",
  "targets": [{"licenceNumber": 102723, "reference": "supplier-001"}],
  "mode": "changesOnly",
  "firstRunBehavior": "emitCurrent"
}
```

1. Add known licence numbers or exact identifiers from product labels.
2. Run the Actor and inspect the first BASELINE observation.
3. Repeat with the same `monitorKey`. Review CHANGE rows; an unchanged check
   returns `outcome=NO_CHANGE`.

The bounded default emits a truthful BASELINE and uses three small source
requests. Repeat the same monitorKey to compare against the last successful
components in the persistent `health-canada-mdall-monitor-state` KVS. Each input
has a result: unchanged inputs emit a compact CHECK with `outcome=NO_CHANGE`;
their full snapshot is omitted in changesOnly mode. Use snapshotAndChanges for
full current observations. A CHANGE row includes the current observation and the
old/new changed components. baselineOnly still emits a baseline observation and
also labels a verified absent first observation BASELINE.

Illustrative change excerpt (simulated, not an actual regulatory event):

```json
{
  "record_type": "CHANGE",
  "status": "SUCCESS",
  "target_key": "LICENCE:102723",
  "change_types": ["LICENCE_STATUS_CHANGED", "LICENCE_SUSPENDED"],
  "licence_status_code": "S",
  "licence_status": "Suspended"
}
```

### Watch licences and device identifiers

```json
{
  "monitorKey": "device-membership",
  "targets": [
    {"licenceNumber": "102723", "includeDevices": true},
    {"licenceNumber": 9538, "includeDevices": true}
  ],
  "mode": "snapshotAndChanges"
}
```

9538 was archived with code O (Discontinued at Renewal) in the 2026-09-06 source
probe; it is not an active demo. includeDevices defaults to false: enabling it
fetches both complete device arrays once per run (about 45 MB decoded,
7.3 MB over gzip and 302,000 rows in the September 2026 probes), then retains only
watched licences. Both active and archived
memberships are included; `active` describes the membership's source state.
Identifiers under every licence are not downloaded wholesale.

```json
{
  "monitorKey": "label-identifiers",
  "targets": [
    {"deviceIdentifier": "09P9422", "reference": "labelled-sku"},
    {"deviceIdentifier": "P02-14030"}
  ]
}
```

Identifiers preserve punctuation, case and leading zeroes; only surrounding
whitespace and Unicode NFC are normalized. Substring API hits must pass exact
comparison. An exact identifier can map to several devices/licences:
`matched_devices` preserves all associations; `matched_device` is null unless
there is exactly one. A moved-licence event requires one old and one new
association with the same device ID. Other association changes are explicit.

### Inputs and limits

1–1,000 targets, each with exactly one identity. Licence numbers are positive
32-bit integers or ASCII digit strings. Identifiers and references are limited
to 200 characters. monitorKey permits 1–80 letters, digits, underscores or
hyphens. Unknown properties are rejected. includeDevices is valid only for
licence targets. Identical duplicate targets receive DUPLICATE without a second
check or charge; conflicting options/reference for one identity invalidate all
copies. Reference changes between runs never create source changes. Enabling
device monitoring creates a component baseline instead of reporting every
existing device as new. The first successful observation of a new or re-enabled
component is included in the Dataset, even in changesOnly mode. If enabling
devices fails, its baseline remains pending until a successful check. Up to 100
exact associations are supported per identifier; larger results fail explicitly
rather than truncate.

### Scheduling, API, webhook and MCP

Use one scheduled run per monitorKey at a time.
For daily 07:00 UTC monitoring, create an Apify Schedule with cron `0 7 * * *`,
timezone `UTC`, and the saved input above. A webhook configuration example is
`{"eventTypes":["ACTOR.RUN.SUCCEEDED"],"requestUrl":"https://your-service.example/mdall"}`.
The receiver should fetch RUN\_SUMMARY and handle PARTIAL even if the platform run
succeeded. Route only `record_type=CHANGE` rows to review/alert integrations.
These examples do not create schedules, Tasks or webhooks.

Example API request:

```bash
curl -X POST 'https://api.apify.com/v2/acts/automa-flow~health-canada-medical-device-licence-monitor/runs?memory=512&timeout=1800&maxTotalChargeUsd=0.02' \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"monitorKey":"procurement-canada","targets":[{"licenceNumber":102723}]}'
```

Direct MCP endpoint:
`https://mcp.apify.com?tools=automa-flow/health-canada-medical-device-licence-monitor`.
Agent intent: “Check licence 102723 for my procurement watchlist, compare the
baseline and return changes or explicit failed components.”
Execution requires the customer's authenticated Apify account. Limited permissions
allow access to Actor-created persistent storage across runs. Authenticated
initialize and tools/list exposed the Actor during validation on 2026-09-07. REST cloud
checks verified repeat-run baseline access; execution and payment through MCP
remain unverified. The observed direct tool
does not expose maxTotalChargeUsd. Use the REST API example above when you need a
specific $0.02 cap; the configured run default is $15 for up to 1,000 checks.
Read RUN\_SUMMARY first, then follow the Dataset link. No source credentials are needed.

### Pricing and spending limits

Pay per event: `target-checked` $0.015 per unique conclusive SUCCESS or
NOT\_FOUND check, with no fixed start or completion fee.
An unchanged verified check is billable; changes never add a second charge.
Failures, partial targets, invalid inputs, duplicates and skipped targets are free.
Apify synthetic start and Dataset events are disabled. Platform usage is included
in the check price. All prices
are USD; no subscription is required.

| Conclusive targets in one completed run | Event total |
| ---: | ---: |
| 1 | $0.015 |
| 10 | $0.150 |
| 100 | $1.500 |
| 1,000 | $15.000 |

Thirty daily 10-target runs cost $4.50; four weekly 100-target runs cost $6.00.
These are event totals, not measured platform costs or revenue forecasts.
One rate applies to all plans. Volume discounts require evidence of paid repeat use
and adequate margins across source conditions.
The Actor uses effective SDK prices when applying
maxTotalChargeUsd. At the limit, unprocessed targets receive SKIPPED and retain
their baseline. Completed observations remain available. Failed and partial checks do
not consume the allowance for conclusive checks; the Actor continues with the
next target while that allowance remains available.

Dataset output is saved before baseline advancement and charging. A durable
delivery checkpoint refuses to replay a run interrupted during delivery: start
a new run for fresh observations. This avoids duplicate charges at the cost of
automatic same-run recovery. KVS write errors fail the run. It is not an atomic
transaction across Dataset, KVS and billing; already delivered checks remain
valid if a later storage operation fails.

### Failure semantics and source freshness

SUCCESS, NOT\_FOUND, PARTIAL, SOURCE\_FAILED, INVALID\_INPUT, DUPLICATE and SKIPPED
are separate statuses. Licence NOT\_FOUND requires both active and archived
lookups to return the validated empty sentinel. Identifier NOT\_FOUND requires
both valid arrays with no exact hit. HTML 200, malformed JSON, unknown status
codes, missing identities, contradictory states, timeouts and exhausted retries
are source failures. Retry limits: three attempts and twenty retries across the
run; long Retry-After values stop work instead of retrying too early.

Licence, company and device components update independently. An unavailable
component is null in the observation and marked SOURCE\_FAILED in component\_status;
its previous successful state and timestamp survive in KVS. Only freshly verified
components create changes. Refresh dates are stored but excluded from hashes;
a regressed refresh quarantines the affected observation. Simultaneous loss of
all previously found licences (at least two) is quarantined as an anomaly.

The API provides no pagination or completeness token. We validate entire bulk
arrays and require exact global-absence corroboration before DEVICE\_REMOVED.
If a missing membership's device still exists under any licence, membership is
PARTIAL and no removal event is emitted. This conservative rule can delay a real
removal. Device active-to-archived transitions are supported directly from bulk
membership evidence. The exact device endpoint ignores state and returns one
association, so it is not used to decide membership status.

The active/archive filter means absence/presence of cancellation/removal dates,
not a safety determination. Licence status labels use the official code table.
Dates describe the public database refresh, which may lag real-world events.
Devices use `(licence_number, device_id)` membership identity; device\_id alone is
not unique across licences. Do not overlap runs using the same monitorKey: KVS
has no atomic compare-and-swap; the stale-write check is not a distributed lock.

Views “Changes”, “Current observations”, and “Not found / partial” expose relevant
columns; use record\_type/status filters to select rows. Output schemas are static.
Records include source, stable source\_id, scraped\_at, schema\_version and a
deterministic fingerprint for downstream append-only history.

### Privacy and limitations

Only company ID, name and status are retained. No addresses, contacts, personal
names, cookies or credentials are copied into output or logs. Compact last-good
state remains in the customer's persistent KVS until they delete it; Dataset
history follows their Apify retention settings.

This Actor reports observations from Health Canada's public Medical Devices Active
Licence Listing (MDALL). It is an independent tool and is not affiliated with or
endorsed by Health Canada. Output should not be treated as legal advice, a product
safety determination, or a guarantee of regulatory compliance.

Sources: [API guide](https://health-products.canada.ca/api/documentation/mdall-documentation-en.html),
[MDALL overview](https://www.canada.ca/en/health-canada/services/drugs-health-products/medical-devices/licences/overview.html),
[open-data catalogue](https://open.canada.ca/data/en/dataset/c801a084-210b-4cd2-8513-26a00b66eb6f).
Contains information licensed under the
[Open Government Licence – Canada](https://open.canada.ca/en/open-government-licence-canada).
The endpoints are `licence/`, `company/`, `device/` and `deviceidentifier/` under
`https://health-products.canada.ca/api/medical-devices/`. Requests are plain HTTPS,
with a single HTTP client, three requests in flight, no browser and no proxy.

### Local development

Run repository pytest, Ruff and mypy checks. Sanitized fixtures never contact the
source in CI. `scripts/local_run.py` runs a manual local SDK invocation with
persistent local storage; `scripts/benchmark.py` measures the source client
without calling a paid Apify service. See the source investigation and benchmark
report under `experiments/health-canada-medical-device-licence-monitor/`.

Version 0.1: initial exact-watchlist implementation, component baselines, typed
changes, conservative removals, bounded PPE and static schemas. Cloud publication
and permission/cost verification remain separate release gates.

# Actor input Schema

## `monitorKey` (type: `string`):

Persistent baseline namespace. Use the same value for repeat checks.

## `targets` (type: `array`):

1-1000 targets; each needs licenceNumber (positive integer or digit string, 1-2147483647) OR deviceIdentifier (1-200 characters). Optional reference (up to 200 characters) and includeDevices (boolean, licence targets only; also checks devices associated with each licence). Each row is validated; unknown fields and conflicting duplicates are marked invalid.

## `mode` (type: `string`):

changesOnly keeps baselines, changes and compact per-input CHECK statuses; snapshotAndChanges includes full current observations.

## `firstRunBehavior` (type: `string`):

Both save a baseline and emit truthful output. baselineOnly labels even a verified absent first observation BASELINE.

## Actor input object example

```json
{
  "monitorKey": "default",
  "targets": [
    {
      "licenceNumber": 102723
    }
  ],
  "mode": "changesOnly",
  "firstRunBehavior": "emitCurrent"
}
```

# Actor output Schema

## `runSummary` (type: `string`):

No description

## `dataset` (type: `string`):

No description

# 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 = {
    "targets": [
        {
            "licenceNumber": 102723
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/health-canada-medical-device-licence-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 = { "targets": [{ "licenceNumber": 102723 }] }

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/health-canada-medical-device-licence-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 '{
  "targets": [
    {
      "licenceNumber": 102723
    }
  ]
}' |
apify call automa-flow/health-canada-medical-device-licence-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automa-flow/health-canada-medical-device-licence-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/fgNGpPQOqlKOesayi/builds/BYctYLynkTeHeItSZ/openapi.json
