# Canada SEMA Sanctions Delta (`titan_coder/canada-sema-sanctions-delta`) Actor

Watches the official Canadian Consolidated Autonomous Sanctions List (SEMA) by country/regime and reports only real changes: new designations and confirmed de-listings. Low, bursty cadence - free when nothing changes.

- **URL**: https://apify.com/titan\_coder/canada-sema-sanctions-delta.md
- **Developed by:** [Radu Furtuna](https://apify.com/titan_coder) (community)
- **Categories:** Business, Automation
- **Stats:** 2 total users, 1 monthly users, 75.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$10.00 / 1,000 sanctions listing changeds

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Canada SEMA Sanctions Delta

Watches the official **Canadian Consolidated Autonomous Sanctions List** (Global Affairs Canada, Special
Economic Measures Act — SEMA) by country/regime and reports only genuine changes since your last check: a
new designation, or a confirmed de-listing. Free when nothing changes — you pay only for delivered events.

Built for compliance/AML/sanctions-screening teams who need to know when someone in their book of business
gets newly designated — or is removed from the list — without re-downloading and diffing a ~2.9 MB XML
file themselves.

### Read this before you rely on the signal — volume is weak, not strong

- **Measured event volume is low.** A direct count of `DateOfListing-DateDinscription` in the live file
  over the last 30 days (checked 2026-09-14) found **8 new designations** across the entire list, all 19
  countries/regimes combined. Some 30-day windows will have zero. This is a **discovery monitor for a
  low-and-bursty signal**, not a high-volume feed — an earlier internal estimate of "26/month" from a
  prior check could not be reproduced against the live file today (same file, same size, same
  Last-Modified as that check) and is not repeated here; treat the 8/30-days figure as the honest current
  baseline, not a promise of steady flow. Do not subscribe expecting daily activity.
- **The source path has already moved once.** The original URL this actor was built against
  (`.../sanctions/sema-lmes.xml` under an older base path) returned 404 on a later check; the current path
  was re-found via the source's own index page. If Global Affairs Canada moves the file again, every watch
  on this actor will start failing with `sema_snapshot_incomplete` (or a permanent fetch error) until the
  actor is updated — this is a real, disclosed operational risk of this particular source, not a
  theoretical one.
- **The file carries no status field at all.** The list contains only *currently active* designations —
  there is no "revoked"/"active" flag and no reason-for-removal field. A de-listed name simply stops
  appearing in the next full download. This actor's entire signal is therefore *presence/absence of a
  record between two snapshots*, not a stated reason for the change.
- **The source has no own stable id.** Identity is a 6-part composite key built from the record's own
  content: country + schedule/annex + last name + given name + entity/ship name + date of birth (or ship
  build date). This key is verified unique across all 5,698 live records (checked 2026-09-14). The
  `Item-NumeroDarticle` field is deliberately **not** part of this key — it is a position within a single
  schedule/annex, not a stable identifier, and can be renumbered when neighbouring entries are added or
  removed at a future republish.
- Because there is no id, **country is part of a record's identity**, not a separate attribute. If Global
  Affairs Canada ever changes how a listing's own `Country-Pays` is spelled, that shows up as an ordinary
  (two-strike) disappearance under the old country and a fresh baseline-free appearance under the new one
  — not as some special "still in the file, just left your filter" event. There is no `left_filter` status
  in this actor's output for that reason (unlike some other actors in this family whose filter field is
  not part of the record's identity).
- De-listing (`disappeared`) detection needs **two consecutive complete downloads** without the record
  (two-strike) — a single missing check only produces an unpaid `removal_candidate` row.
- If this run's XML download looks truncated, malformed, or suspiciously small
  (`coverage.semaSnapshotIncompleteCount > 0`), **no baseline is initialized and no de-listing detection
  runs at all** for the affected watches — a partial snapshot never gets silently treated as "nothing
  changed" or "everyone was de-listed." Nothing is billed in that case.
- This is an **informational monitor of a public sanctions list**, not a real-time compliance-screening
  service and not a substitute for checking directly with Global Affairs Canada
  (international.gc.ca/world-monde/international\_relations-relations\_internationales/sanctions) before
  making a business or compliance decision.

### How it works

1. First run per watch (`monitorId` + `watchId`) records a private baseline of every record currently
   matching your `country` filter — no rows, no charge. A baseline is only recorded from a **complete**
   download (see integrity checks below).
2. Every later run re-downloads the full list and classifies each record in your filter:
   - **`appeared`** — a new record matching your filter appeared (new designation, or a name returning
     after a confirmed prior de-listing);
   - **`removal_candidate`** — a record you'd seen before is missing from this run's download for the
     first time in a row. Delivered as an informational row with `billed: false` — not charged, and not
     proof of anything (a single missing point is indistinguishable from a transient download hiccup);
   - **`disappeared`** — the same record is missing for the **second consecutive** complete download.
     Billed: the designation was very likely removed from the list.
3. Only `appeared`/`disappeared` are billed, one PPE event each (`sanctions-listing-changed`).
   `removal_candidate` is always informational, never billed.
4. **Attribute changes on a record that stays listed and stays inside your filter are never reported as
   an event.** This actor is a discovery/membership monitor, not an attribute-change tracker: the source
   itself carries no revision history for a listing's non-identity fields (aliases, ship IMO number,
   etc.). Only presence/absence relative to your filter is ever reported.

### Download integrity (why a partial file can never become a false signal)

The list is one XML file, no pagination — there is no way to fetch "the next page" if a download is
truncated, so this actor checks completeness independently, five ways, before trusting a download for
baseline or de-listing purposes at all: the server's `Content-Length` header must be present and numeric;
the actually-downloaded byte count must match it exactly; the XML must parse as well-formed and complete
(a `ParseError` — the typical symptom of a stream cut off mid-tag — is never silently tolerated); the
final record count must clear a floor (currently 3,000 — the live file has 5,698); and **the file is
downloaded twice, over two independent connections, and the two results must match byte-for-byte** (see
below). Any failure discards the whole snapshot for every watch in the run; none of these are about how
many records changed, only about whether the download itself can be trusted.

**Known limitation: the double-download check is a weaker substitute for an independent completeness
marker, not an equivalent.** Unlike some sibling actors in this family whose source publishes through a
catalog wrapper (e.g. a CKAN `package_show` call that can report a resource's expected size *before*
downloading it), `international.gc.ca` serves this file as a plain static XML with no independent,
third-party record of what a complete download should look like — no catalog entry, and the file's own
human-readable landing page is not reliable either (its displayed date was observed to disagree with the
file's own `Last-Modified` by six days during construction). To reduce (not eliminate) the risk of a
truncated file passing every check unnoticed, this actor downloads the same URL **twice**, each over its
own separate connection (a fresh `requests.Session()` per attempt, no connection reuse between the two),
and requires the two full response bodies to be byte-for-byte identical in addition to each individually
passing all four checks above. If they disagree, the run treats the snapshot as untrustworthy and uses
neither of the two downloads. This closes the specific failure mode where a truncated response is
internally self-consistent (well-formed XML, correct Content-Length for its own — shorter — body) and
would otherwise slip past the first four checks. It does **not** close the case where the same
intermediary (a CDN edge, or a corporate/network proxy sitting between wherever this actor runs and
`international.gc.ca`) truncates **both** independent requests identically — in that scenario the two
downloads would still match each other while both being wrong, and this check would not catch it. A live
DNS/TLS check at construction time (single A record, no CDN-typical headers, certificate issued to
`Shared Services Canada`) found no evidence of an edge CDN in front of this source, which lowers — but by
itself does not prove — the likelihood of this residual scenario.

### Input

- `monitorId` — name of this monitor's durable history (`a-z0-9-`, up to 40 chars). Re-run with the same
  `monitorId` to continue the same history; a new value starts a fresh baseline.
- `watches` — 1-20 objects `{"watchId", "country"}`. `country` is matched as an exact string (after
  trimming leading/trailing whitespace) against the source's own bilingual `Country-Pays` field, or the
  special value `ALL` for the whole list with no filter. This field is **not** a closed enum — Canada adds
  new sanctions regimes from time to time (the live file already carries non-country entries such as a
  corrupt-foreign-officials-justice regulation) — use the exact string as the source publishes it. Once a
  `watchId` is used, its `country` is locked — reusing the id with a different filter fails the run
  instead of silently mixing histories. Add new `watchId`s any time under the same `monitorId`.
- `notifyOn` / `webhookUrl` — optional HTTPS webhook receiving a JSON digest of billed changes. The webhook
  is only sent if a conservative worst-case time budget for the request itself still fits before the run's
  deadline; if it does not, the webhook is skipped entirely rather than risking a late/contradictory
  delivery, and the run honestly reports `run_wall_timeout`.

### Output

- **Dataset** — every row this run produced (`watchId`, `compositeKeyFingerprint`, `status`, `country`,
  `lastName`/`givenName` or `entityOrShip`, `dateOfBirthOrShipBuild`, `schedule`, `itemNumber`,
  `dateOfListing`, `aliases`, `shipImoNumber`, `titleOrShipType`, `eventId`, `billed`). `status` is one of
  `appeared`/`disappeared`/`removal_candidate`. `compositeKeyFingerprint` is a SHA-256 hash of the record's
  6-part identity key (see above) — used internally for claim/event deduplication and shown here so you
  can join rows belonging to the same underlying record across runs. A durable named dataset holds the
  append-only history of everything ever delivered under this `monitorId`; the default (run) dataset
  mirrors what this run delivered plus a `run_summary` row.
  - `billed` on a `removal_candidate` row is always `false` — never charged, by design.
  - `billed` on an `appeared`/`disappeared` row is written as `null`. The row is saved to the dataset
    *before* its charge is attempted (we never charge before delivery is confirmed — see "Guarantee"
    below), so the final charge outcome isn't known yet at write time. To find out whether one specific
    row was actually charged, match its `eventId` against this run's Pay-Per-Event charged-events ledger
    in the Apify Console. For the run-level total, use `recordsDelivered`/`recordsBilled` in `coverage`
    and the unbilled count in `digest`.
- **Key-value store** — `coverage` (per-watch status/reason/matchedCount/candidateCount, requested/
  attempted/succeeded/failed, records delivered/billed, total list records seen this run;
  `semaSnapshotIncompleteCount > 0` means the download was rejected as incomplete for the affected watches
  — see "Honest limits") and `digest` (a short human-readable summary, which also carries this
  partial-coverage warning even when no billable change occurred).

### Guarantee

**At-most-once delivery and billing, not exactly-once.** A crash right after a right-to-bill is granted
can lose that one event (it will not be redelivered), but you will never be charged twice for the same
underlying change. See `ROADMAP.md` for the full claim protocol and its boundaries.

### Durable dataset vs. default dataset

The **durable named dataset** is a journal of delivery attempts across every run of this `monitorId` — it
is not itself proof of payment (a row can in rare crash scenarios be written without a confirmed charge).
The **default (per-run) dataset** mirrors this run's rows plus one `run_summary` record and is what you
see in the Apify Console run view. Reconcile billing against `coverage`, not row count alone.

# Actor input Schema

## `monitorId` (type: `string`):

Name of this monitor's durable history (a-z, 0-9, dash; up to 40 chars).

## `watches` (type: `array`):

1-20 objects: {"watchId": "russia-designations", "country": "Russia / Russie"}. country is matched as an exact string (after trimming leading/trailing whitespace) against the source's own bilingual Country-Pays field, or the special value ALL for the whole list with no country filter. The source's own field also carries sanctions-regime names that are not literal countries (for example a corrupt-officials-justice regulation) - use the exact string as it appears in the source dataset. New watches can be added later under the same monitorId.

## `notifyOn` (type: `string`):

new\_alerts — post the webhook only when new billed changes were delivered; always — post it every run; never — do not call webhookUrl at all.

## `webhookUrl` (type: `string`):

Optional. Receives a digest of delivered (billed) sanctions listing/de-listing changes as JSON. HTTPS only.

## Actor input object example

```json
{
  "monitorId": "my-sema-watch",
  "watches": [
    {
      "watchId": "russia-designations",
      "country": "Russia / Russie"
    }
  ],
  "notifyOn": "new_alerts"
}
```

# Actor output Schema

## `results` (type: `string`):

Every row this run produced. Key fields: watchId, compositeKeyFingerprint (SHA-256 of the composite identity key - the source has no own stable id, see README), status (appeared|disappeared|removal\_candidate), country, lastName/givenName or entityOrShip, dateOfBirthOrShipBuild, schedule, itemNumber, dateOfListing, aliases, shipImoNumber, titleOrShipType. removal\_candidate rows carry billed:false (unpaid early signal from a single missing snapshot, not yet confirmed - confirmed only after two consecutive full snapshots miss the record). Informational monitor of a public sanctions list - not a real-time compliance-screening service, and not an attribute-change tracker (the source carries no status/reason field at all - it lists only currently active designations). Note: country is part of the record's identity key, so if the source ever changes a listing's own Country-Pays spelling, that shows up as an ordinary two-strike disappearance under the old country, not as a special filter event.

## `coverage` (type: `string`):

What this run actually covered and what it charged for: per-watch status/reason/matchedCount/candidateCount, records delivered and records billed, requested/attempted/succeeded/failed watch counts, total list records seen. semaSnapshotIncompleteCount > 0 means the XML download looked truncated, malformed or suspiciously small this run - no baseline and no de-listing detection happened for the affected watches, and nothing was billed for them.

## `digest` (type: `string`):

A short human-readable summary of what this run found, written every run.

# 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 = {
    "monitorId": "my-sema-watch",
    "watches": [
        {
            "watchId": "russia-designations",
            "country": "Russia / Russie"
        }
    ],
    "notifyOn": "new_alerts"
};

// Run the Actor and wait for it to finish
const run = await client.actor("titan_coder/canada-sema-sanctions-delta").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 = {
    "monitorId": "my-sema-watch",
    "watches": [{
            "watchId": "russia-designations",
            "country": "Russia / Russie",
        }],
    "notifyOn": "new_alerts",
}

# Run the Actor and wait for it to finish
run = client.actor("titan_coder/canada-sema-sanctions-delta").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 '{
  "monitorId": "my-sema-watch",
  "watches": [
    {
      "watchId": "russia-designations",
      "country": "Russia / Russie"
    }
  ],
  "notifyOn": "new_alerts"
}' |
apify call titan_coder/canada-sema-sanctions-delta --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,titan_coder/canada-sema-sanctions-delta"
        }
    }
}
```

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/gAyrzFc3rjjCYRkfm/builds/Jh2OQCrcYINqSueS2/openapi.json
