Paris MoU Ship Detention Monitor avatar

Paris MoU Ship Detention Monitor

Pricing

$10.00 / 1,000 detention events

Go to Apify Store
Paris MoU Ship Detention Monitor

Paris MoU Ship Detention Monitor

Watches the official Paris MoU / EMSA THETIS list of ships detained after port state control. Charges only for a real event: new detention, confirmed release, or an edited detention record. Track your fleet by IMO, flag or port state. For owners, charterers, insurers, P&I clubs.

Pricing

$10.00 / 1,000 detention events

Rating

0.0

(0)

Developer

Radu Furtuna

Radu Furtuna

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

5 days ago

Last modified

Share

Durable monitor for the official Paris MoU port state control detention list, published by the European Maritime Safety Agency (EMSA) through the THETIS portal that powers the inspection search on parismou.org. Watch your own fleet by IMO number, a flag, a port state — or the whole list — and get notified only when something genuinely happens: a ship is detained, a detention record is edited, a ship is detained again, or a detention is lifted. No API key, no login, no captcha.

Built for ship owners and managers, charterers and chartering brokers, marine insurers and P&I clubs, class and flag administrations, and maritime compliance teams — anyone for whom "one of our ships (or a ship we are about to fix) is under detention in Rotterdam" is a same-day decision, not a monthly report.

Source

https://portal.emsa.europa.eu/o/portlet-public/rest/detention/getCurrentDetentions.json — the public, anonymous REST layer of the EMSA portal behind the official Paris MoU inspection search widget. Verified live 13.09.2026 from a plain HTTP client, no credentials of any kind:

  • HTTP 200, 552,533 bytes, {"results": [...], "total": 49, "success": true}49 ships under detention right now, total always equal to the number of records returned.
  • id is a stable numeric identifier (e.g. 7491791432) and is unique inside the response (49 unique ids across 49 records). The same id is accepted by getInspectionDetail.json?inspectionId= — verified.
  • Every record carries imoNumber (exactly 7 digits, zero deviations across all 49), shipName and detentionDate (strictly dd/mm/yyyy) — those four are treated as mandatory identity and a missing one fails the run closed.
  • shipType, detentionPort, detentionReportingAuthority and flag are delivered as attributes and may legitimately be null: we observed a ship (LIAM, IMO 7917874) published with flag: null — a ship with no flag state. They are still part of the billing hash, so a flag appearing later is a real, billable change to the record.
  • flag.performanceType (WHITE / GREY / BLACK_*) is present on 41 of 49 records and legitimately absent on 8 — it is delivered as context and deliberately excluded from billing.
  • getInspectionDetail.json?inspectionId=<id> returns the full inspection card: deficiency codes (defectiveItem.code, deficiencyArea), groundForDetention, ismRelated, ISM company (with its own IMO number), gross tonnage, ship age, inspection type and detention duration.
  • Volatility (measured, not estimated): 8 new detentions in the last 7 days, 16 in 30 days; per month in 2026: 06 — 2, 07 — 3, 08 — 3, 09 — 13 (in 13 days). Roughly the same number of releases, since a released ship simply disappears from the list.
  • EMSA's disclaimer (emsa.europa.eu/disclaimer.html) is the standard European Commission text and contains no restriction on commercial use.

How it works

  1. The detention list is fetched once per run — it is one document covering the whole Paris MoU region (Europe and the North Atlantic), so every watch filters that same snapshot instead of hammering the EMSA portal 30 times for identical bytes.
  2. Each watch names one slice: imo (your fleet, 1-200 IMO numbers), flag, authority (the port state that reported the detention), or all.
  3. The first check of a new watch establishes a baseline (no charge). Every later check compares the current slice against the durable record of what it was last time.
  4. Billing is tied only to the detention record: IMO, ship name, ship type code, detention date, port LOCODE and country, reporting authority, flag code. It deliberately excludes the Paris MoU flag performance rating (which is revised centrally and would otherwise "change" every ship under that flag at once) and all human-readable descriptions. Those fields are still delivered in every row.
  5. A ship that disappears from the list is not billed immediately. The first run that misses it emits an unpaid release_candidate row; the release is confirmed and charged only if the ship is still missing on the next accepted snapshot. A single truncated response from the source must never be paid for as "the ship was released".
  6. A state that changes and later reverts bills every genuine transition (detained → released → detained again → released), never silently deduplicated against an earlier occurrence of the same state.

Input

{
"monitorId": "my-fleet-watch",
"watches": [
{ "watchId": "my-fleet", "scope": "imo", "imoNumbers": ["9498315", "9146053"] },
{ "watchId": "liberia-flag", "scope": "flag", "flagCode": "LR" },
{ "watchId": "uk-ports", "scope": "authority", "authorityCode": "GB" }
],
"includeInspectionDetail": true,
"notifyOn": "new_alerts",
"webhookUrl": "https://example.com/webhook"
}

Add more slices later under the same monitorId — each watch keeps its own independent history. A watchId is permanently bound to the slice it first saw; pointing the same watchId at a different slice later fails the run instead of silently mixing histories.

Output row (per change)

watchId, watchScope, watchTarget, detentionId, changeType ("detained"|"re_detained"| "detention_updated"|"released"|"release_candidate"), imoNumber, shipName, shipType, detentionDate, previousDetentionDate, portCode, portName, portCountryCode, detainingAuthority, detainingAuthorityName, flagCode, flagName, flagPerformance, deficiencyCount, detainableDeficiencyCount, groundsForDetention[], deficiencies[], detentionType, detentionDurationDays, banned, ismCompanyName, ismCompanyImo, grossTonnage, shipAge, inspectionDetailStatus, firstMissingAt, lastSeenAt, sourceUrl, contentHash, monitorId, runId, discoveredAt, eventId, billed

Billing

Pay-per-event: detention-event — charged for a new detention, a re-detention, an edit to a detention record, or a confirmed release. The baseline run of a watch is free, release_candidate rows are free, and failed, blocked or structurally suspect checks are never charged.

Important — read before relying on this for any chartering, insurance or compliance decision

This is a monitor of changes to an EMSA/Paris MoU publication, not an assessment of a ship's seaworthiness and not an insurance, class or flag determination. A ship appearing here has been detained by a port state after inspection, as published; a ship disappearing from the list means only that the publication no longer lists it, which is not proof that the ship has physically been released or that its deficiencies were rectified. Publication can lag the real-world event in either direction. This actor is an informational monitor — it is NOT legal, commercial, insurance or technical advice, and NOT a substitute for a direct check at parismou.org and with the port state authority concerned before fixing, insuring, releasing or reporting on any single vessel.

Delivery guarantee: at-most-once (we would rather lose an alert than bill you twice)

Each computed change is delivered to the dataset and charged at most once, for as long as the monitor's claim log exists (see the boundary below). Before any irreversible step (writing the row, charging the event) the run takes an atomic claim on that exact change, using the only atomic primitive the Apify platform offers: a request queue's unique-key insert. Exactly one run can win that claim for a given change. The claim log is never consumed, deleted or rotated by this actor; it is a permanent record of what was already attempted, and coverage.claimJournalSize reports its size each run so you can watch it grow (the platform's counter is eventually consistent, so treat it as a lagging estimate, not an exact count).

Where that guarantee ends — the honest boundary. The claim log lives in a named request queue (<prefix>-<monitorId>-claims) in your own account. The at-most-once guarantee holds as long as that queue keeps existing. If you — or any process holding your account credentials — delete, rename or re-create it from the Console or API, the log starts empty and previously delivered changes can be delivered and charged again. That is the unavoidable boundary of any durable storage, not a loophole in the protocol. For the same reason, the actor's storage prefix and internal claim namespace are frozen after release: changing either would create a fresh, empty log with exactly the same effect.

The response the platform returns for each claim is interpreted strictly: only a real boolean false grants the right to write and charge, only a real boolean true denies it, and anything else — a missing field, null, 0, an empty string, a changed SDK response shape — aborts the run's delivery for that item with claim_protocol_error before any row or charge. An answer we do not fully understand is never read as "you may charge".

One thing we deliberately do not claim: the monitor's lease makes overlapping runs a fail-closed exception rather than a fact of life, but between the moment a run verifies it still holds the lease and the moment the dataset write or charge actually lands there is an unavoidable time gap (the platform offers no fencing token for datasets or billing). So "a run that lost the lease can never write another row" would be an overstatement. What actually protects your money is the claim above: the key is already taken, so even a ghost run cannot charge for the same change twice.

The honest consequence, stated plainly: if a run dies after taking the claim but before finishing, that one change is lost. It is recorded as dataset_unknown or charge_unknown and it is not re-delivered on the next run — the next run moves on to that ship's next change. We deliberately chose possible loss of one alert over the possibility of charging you twice for the same event. This is at-most-once delivery, not exactly-once; any actor that claims exactly-once over a store without compare-and-swap is overstating what the platform can do.

Practically this only happens if the Apify run is killed mid-delivery (platform abort, timeout, migration). Every such case is visible: the run's coverage and run_summary report it, and run_summary.eventsBilled plus Apify's own billing ledger remain the source of truth for what you actually paid for.

Honest limits

  • The durable dataset is a delivery-attempt log, not a guaranteed mirror of the default dataset. Each row is written to the durable dataset first, then mirrored to the run's default dataset before billing proceeds for that row. If the durable write succeeds but the default-dataset mirror write fails (e.g. transient Apify storage error), the item is marked dataset_unknown, billing for it is permanently blocked (fail-closed — we never charge for a row we can't confirm was delivered), and the run is not retried into re-creating that exact row. The durable dataset can therefore end up with a small number of orphan rows that were never mirrored and never billed. The default dataset is the canonical log of rows successfully written to this run's output (see its run_summary row) — but a default-dataset row does not by itself prove the row was billed: the row is written before Actor.charge() runs, so if charging then fails or comes back charge_unknown, the row is present but not confirmably paid. run_summary.eventsBilled and Apify's own billing ledger are the source of truth for confirmed payment, not the presence of a row in either dataset.
  • This is the current detentions list, not a historical archive and not the inspection stream. It answers "which ships are detained right now, and what changed since my last check". It does not back-fill detentions that ended before your first run, and it does not report inspections that produced no detention. A ship's inspection card is fetched only to enrich an event that already happened.
  • A release is inferred from disappearance, and confirmed over two runs. The source publishes no "released" flag: a ship that is let go simply stops appearing. We therefore require two consecutive accepted snapshots without the ship before charging a released event, and we emit an unpaid release_candidate row after the first one. The practical cost is that a release is reported one run late; the practical benefit is that a truncated response from the portal is never sold to you as a release. On the other side, a released event still means "no longer published", not "verified back at sea".
  • Truncated source responses are refused, not interpreted. If total disagrees with the number of records returned, if the list falls below an absolute floor, if the whole list shrinks by more than 40% against the previous accepted snapshot, or if a large slice shrinks by more than 40%, the run reports source_access_limited for the affected watches and updates nothing — no tombstones, no billing. Small slices (your own fleet of one or two ships) are deliberately not subject to the per-slice test, since a fleet slice legitimately drops to zero when your ship is released; they are protected by the whole-list test instead.
  • Inspection detail is best effort, with one exception. This API answers a non-existent inspectionId with HTTP 500, so "no such card" and "the portal is down" are indistinguishable — a failed enrichment therefore never cancels the detention event itself (inspectionDetailStatus tells you why it is missing). The exception is a card whose id or ship IMO does not match what we asked for: foreign deficiency codes inside a paid row are worse than no enrichment, so that fails the watch closed. Enrichment is also capped per run (40 cards) — beyond that, events are still delivered with inspectionDetailStatus: budget_exhausted.
  • A run that checks some watches but not others tells you so. If one slice is checked fine and another is not trustworthy, the run still finishes as SUCCEEDED, but its reason becomes partial_watch_failures: N/M, coverage.failedReasons lists why, and the digest and webhook carry an explicit "result is INCOMPLETE" line. "No changes found" and "we could not look" are never reported as the same thing.
  • A flood of changes is capped per watch per run (50), so a mass shift on the source side cannot drain your budget in one run; the remainder is picked up by the next run, because the durable index only advances for changes that were actually delivered.
  • The monitor holds a lease, so a second run of the same monitorId stops instead of running in parallel. If a run dies, the lease is released after a grace window (not after the run's full timeout), so the next scheduled run takes over promptly instead of failing as "monitor busy". The lease is a mutex for orderly behaviour, not the thing that protects your billing — see the time-gap note in the delivery guarantee above.
  • Billing tracks the detention record only, deliberately excluding the Paris MoU flag performance rating and all descriptive text. Those fields are still delivered in every row for context.
  • We don't invent data: if the response is not a JSON object with success: true, if results is not a list, if total disagrees with the record count, if an id repeats, if a required column has vanished from every record (as opposed to being null in one), if imoNumber is not already exactly 7 digits, or if detentionDate is not dd/mm/yyyy, the run reports it honestly (source_access_limited) instead of guessing what it actually found.

Author: OmniCoder (https://t.me/OmniCoder)