All Articles
How-To

How to Automate VGM Submission for Container Shipping

A step-by-step workflow for automating VGM capture, matching, submission and exception handling, with the code and logic you can build from today.

How to Automate VGM Submission for Container Shipping

VGM is a hard cut-off. Miss it and the container doesn't load, full stop, no appeal to the terminal. Most forwarders handle it the way they've always handled it: someone emails a weighbridge ticket, someone else re-types the number into a carrier portal, and the whole chain depends on nobody being on leave that week. This article is the workflow I run to take the humans out of the repetitive parts and keep them only where judgement is actually needed: the exception queue.

Why manual VGM breaks at volume

The SOLAS VGM requirement (IMO, in force since 2016) gives you two legal ways to get the number: Method 1, weighing the packed container, or Method 2, weighing the cargo and adding the container's tare weight. Either way, the number has to reach the carrier before their cut-off, which is usually earlier than the physical gate cut-off.

The manual version of this process has three places it fails:

  1. The weight document arrives in a format nobody parses consistently: a PDF weighbridge ticket, a photo from a driver's phone, a packing list with a tare weight buried in a footnote.
  2. The weight gets attached to the wrong container or booking number because someone copy-pasted from the wrong email thread.
  3. Nobody notices a VGM is missing until the carrier's own deadline reminder lands, by which point there's no time left to chase it.

None of these are technology problems in the "we need AI" sense. They're data-plumbing problems. Fix the plumbing and the deadline stops being scary.

The four-stage automation workflow

1. Capture weight data at the source

Treat every VGM input, whether it's a weighbridge ticket, a packing list, or a manual entry form, as a structured record the moment it enters your system. Don't let it sit as an attachment waiting for a human to read it.

{
  "source_type": "weighbridge_ticket",
  "container_no": "MSCU1234567",
  "booking_no": "BKG998877",
  "gross_weight_kg": 24850,
  "method": "1",
  "weighed_at": "2026-10-20T14:32:00Z",
  "weighing_party": "ACME Weighbridge Ltd",
  "raw_document_id": "doc_7f3a2b"
}

If the document is OCR'd rather than entered manually, flag the confidence score on the fields that matter (container number, weight) and route anything below your own threshold straight to a human reviewer instead of trusting it blind.

2. Match the weight to the right container and booking

This is the step that fails most often in manual workflows, so make it deterministic. Match on container number first (it's the most reliable key, format-checked against ISO 6346), then cross-check against the booking number on file.

def match_vgm_record(record, bookings):
    container = normalise_container_no(record["container_no"])
    candidates = [b for b in bookings if b.container_no == container]

    if not candidates:
        return route_to_exception(record, reason="no_matching_booking")

    if len(candidates) > 1:
        return route_to_exception(record, reason="ambiguous_match")

    booking = candidates[0]
    if record.get("booking_no") and record["booking_no"] != booking.booking_no:
        return route_to_exception(record, reason="booking_mismatch")

    return attach_vgm(booking, record)

Run the ISO 6346 check digit validation on every container number before it touches this logic. A typo'd container number is the single most common reason a correct weight ends up on the wrong shipment. (Our container number validator runs exactly this check if you want to test a batch before wiring it into your own pipeline.)

3. Validate before you submit anything

Don't let a bad number reach the carrier. Basic sanity checks catch most of the obvious errors before they become a rejected submission:

def validate_vgm(record, container_spec):
    errors = []
    if record["gross_weight_kg"] <= container_spec.tare_weight_kg:
        errors.append("gross_weight_below_tare")
    if record["gross_weight_kg"] > container_spec.max_gross_kg:
        errors.append("exceeds_container_max_payload")
    if record["method"] not in ("1", "2"):
        errors.append("invalid_method")
    return errors

Anything that fails validation goes to the exception queue, not to the carrier. A rejected VGM submission at the carrier's end wastes more time than catching it yourself five minutes earlier.

4. Submit via carrier portal API or EDI, before cut-off, not at cut-off

Most carriers accept VGM via a portal, an EDI message (commonly carried over the same channels as other shipping instructions), or an API where one's published. Whichever channel you use, build the submission as a queued job with its own deadline, separate from the terminal's gate cut-off:

def schedule_submission(booking, vgm_record):
    deadline = booking.carrier_vgm_cutoff
    buffer = timedelta(hours=4)
    submit_by = deadline - buffer
    queue_submission(booking, vgm_record, run_at=min(now(), submit_by))

The buffer matters more than the automation itself. A bot that submits at the exact deadline is still fragile to a carrier portal outage. Give yourself room to retry.

The exception queue is the real product

Automating the happy path is the easy half. The queue that catches everything that didn't match, didn't validate, or didn't arrive in time is where the actual risk management lives. At minimum it needs:

Exception typeTriggerAction
No VGM receivedBooking within N hours of cutoff, no weight on fileAlert ops, flag shipper
Ambiguous matchMultiple bookings share a container prefix patternHuman review
Validation failureWeight outside tare/max boundsReturn to source for correction
Submission failureCarrier portal/EDI rejects or times outRetry with escalating alert

Set the "no VGM received" trigger well before the carrier's cutoff, not at it. If your cutoff is, say, 24 hours before vessel departure, alert at 48 hours so there's still a working day to chase the shipper.

Worked example: run this on your own volume

Take your own numbers and run the arithmetic:

shipments per month handled manually        = A
average minutes per VGM chase/entry/submit  = B
minutes saved per shipment once automated   = B - (automated handling minutes)
hours saved per month = A × (B - automated handling minutes) / 60

If a rolled container costs you real money in re-booking, demurrage, or client goodwill, the number that matters most isn't the time saved on the easy cases. It's how many fewer times per quarter a VGM miss causes a roll. Pull your own rolled-container log, count how many rolls over the last few quarters were caused by a missing or late VGM, and use that figure, together with your own re-booking and demurrage costs per incident, to work out what a tighter exception queue would be worth to you.

Where this fits with CargoWise

If your bookings and container data already live in CargoWise, the matching and validation logic above maps cleanly onto shipment and container records you're already maintaining. The eAdapter series covers how to wire CargoWise data into exactly this kind of automated pipeline, including the event hooks you'd use to trigger a VGM capture the moment a container number is confirmed.

Run a batch of your current container numbers through the container number validator to see how many would have failed the ISO 6346 check before ever reaching a carrier portal, and go from there.

Ready to automate your document processing?

Join freight forwarders saving hours every week with CargoMode.

Start for free