Solutions · Port Calls API
Arrivals, departures and time in port
Every stop a vessel makes in a port, derived from its AIS positions: when it arrived, when it left and how long it stayed. Filter by port or vessel, and get a webhook for each arrival and departure.
- Proof of delivery and ETA updates for shippers
- Port-call history for chartering
- Demurrage and laytime tracking
# Calls by port, mmsi, status (in_port or departed) and since curl "https://seasignals.com/v1/port-calls?port=NLRTM&status=in_port" \ -H "Authorization: Bearer $SS_KEY"
Endpoints
Query it whenever you need it
GeoJSON responses with typed IDs, ISO 8601 times and units in every field name. Errors come back as {"error": {"code", "message"}}.
GET
/v1/port-callsCalls by port, mmsi, status (in_port or departed) and sinceGET
/v1/portsPorts with UN/LOCODE ids and whether they're in live coverage
Webhooks
Or let the events come to you
Subscribe a URL once and SeaSignals POSTs a signed JSON event whenever something changes. Sent within about 10 minutes of the vessel stopping or leaving.
EVENT
port_call.arrivedA vessel stopped inside a watched portEVENT
port_call.departedIt left the port, with its total stayFilters
ports | UN/LOCODEs, e.g. ["NLRTM", "BEANR"]; all ports when left out |
mmsi | Only these vessels |
curl -X POST https://seasignals.com/v1/webhooks \ -H "Authorization: Bearer $SS_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/seasignals", "product": "port_calls", "events": [ "port_call.arrived", "port_call.departed" ], "filters": { "ports": [ "NLRTM", "BEANR" ] } }'
{
"id": "evt_3kV9q2mXw8rT1yLp0aZc4nB7",
"type": "port_call.departed",
"created_at": "2026-10-29T09:00:12Z",
"webhook": "webhook:42",
"data": {
"id": "port_call:8812",
"status": "departed",
"port": {
"id": "port:NLRTM",
"name": "Rotterdam",
"country": "NL"
},
"vessel": {
"id": "vessel:mmsi:244123456",
"mmsi": 244123456,
"name": "NORDIC STAR",
"type": "cargo"
},
"arrived_at": "2026-10-27T22:10:00Z",
"departed_at": "2026-10-29T06:40:00Z",
"stay_hours": 32.5
}
}
Delivery
Signed, retried, logged
- Signed: every request carries
SeaSignals-Signature, an HMAC-SHA256 of the timestamp and body with your webhook secret. - Retried: anything but a 2xx within 10 seconds is retried after 1 min, 5 min, 30 min, 2 h and 6 h.
- Logged:
GET /v1/webhooks/{id}shows recent deliveries, andPOST /v1/webhooks/{id}/testsends a test event. - Safe to retry: use the event
idto ignore a delivery you've already handled.
// header: SeaSignals-Signature: t=1761729600,v1=5f2b… const [t, v1] = header.split(",").map((p) => p.split("=")[1]); const expected = crypto.createHmac("sha256", process.env.SS_WEBHOOK_SECRET) .update(`${t}.${rawBody}`).digest("hex"); const ok = crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) && Date.now() / 1000 - Number(t) < 300;
import hashlib, hmac, os, time t, v1 = (part.split("=")[1] for part in header.split(",")) expected = hmac.new(os.environ["SS_WEBHOOK_SECRET"].encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest() ok = hmac.compare_digest(v1, expected) and time.time() - int(t) < 300
t, v1 = header.split(",").map { _1.split("=").last } expected = OpenSSL::HMAC.hexdigest("SHA256", ENV["SS_WEBHOOK_SECRET"], "#{t}.#{raw_body}") ok = ActiveSupport::SecurityUtils.secure_compare(v1, expected) && Time.now.to_i - t.to_i < 300
Try the Port Calls API free
The free trial includes every API and up to 5 webhooks, 1,000 requests a day and 14 days of history.