~/mesha-vegas$⌘
$ cd ../

2026-10-30

Automating a website that has no API: the agent has to know when to stop

When the only way into a system is a web form, you can drive it with a browser. The hard part isn't filling in the form: it's knowing when not to click.

  • #automation
  • #playwright
  • #rpa
  • #reliability
  • #logistics
  • #architecture
$8 min read9 reads

A driver reaches the port gate at 5 a.m. To get in, he needs an access code, issued after a request filed on the port's portal. If the request wasn't filed, he waits. If it was filed with the wrong licence plate, he waits too, and the container he's hauling waits with him.

At MOVELT, a haulage company that moves containers to the Port of Kribi, those requests were typed in by hand by an assistant, often at night. The portal has no API. The only way to file a request is its web form.

I built the agent that fills it in instead. What follows isn't a Playwright tutorial: filling in fields is the easy part. It's what I learned about the hard part, deciding when not to act.

First, define what you're really after

The specification talked about "speeding up access requests". That wasn't the real problem. The harbour master's approval takes as long as it takes, and isn't up to us.

The real problem was the night-time data entry. A person woken up to copy plates and container numbers into a form. So the success criterion became: nobody types anything at night any more.

It sounds like a nuance. It drove every trade-off that followed. When you're torn between faster and safer, you know which one you pick.

The imbalance that decides everything

In most software, a mistake costs a support ticket. Here:

  • a request that isn't filed costs a call to the assistant, who files it by hand;
  • a wrong request blocks a truck at the gate.

The first mistake is recoverable. The second isn't. The rest of the design follows from that imbalance: the agent would rather stop than guess.

Read over HTTP, write with a real browser

Two operations, two tools.

To read the status of requests, the portal's list page is server-rendered HTML. An HTTP request with the right session is enough. No browser, almost nothing to execute: you can check it every two minutes at no cost.

To write, that is to file a request, we go through a real Chromium driven by Playwright. The temptation would be to reproduce the form's POST request by hand: faster, lighter. But it breaks at the site's first change, and this site isn't ours. Nobody will warn us about its updates. A browser filling in the form like a human survives far more changes than a hand-crafted request.

Everything we know about the portal (selectors, labels, page markers) lives in a single module. The day the port changes its form, there's one place to fix.

Recognise the page before touching anything

The agent's first reflex: check that it is where it thinks it is. Every expected block of the form is looked for before the first click. If one is missing, the agent gives up, naming the missing block in the error message.

That matters more than it seems. A maintenance page, an expired session bouncing back to the home page, a new version of the form: in every one of those cases, a naive script would fill in the wrong fields.

Second discovery: the order of filling matters. On this form, choosing the dates triggers a load that populates the driver and vehicle lists. Filling in the driver before the date means searching a list that's still empty. No documentation says so: you learn it by watching the site work.

Read back before clicking

Before the final click, the agent reads the whole form back and compares it with the original request. Driver, vehicle, dates, container: everything must match. At the slightest difference, it gives up. Never a partial submission.

And it takes a screenshot just before the click, kept along with the page's HTML. On a system you have no direct read access to, that's the only way to understand a failure after the fact. It's also the evidence you produce if the gate disputes the request.

Three verdicts, not two

After the click, the natural question is: did it work? You expect two answers, yes or no. There are three:

  • accepted: the request exists at the port;
  • rejected: it doesn't, you can fix it and try again;
  • undetermined: the page didn't respond as expected.

The third is the dangerous one. The request may exist. Retrying automatically could create a second one, and the driver would turn up with a code that isn't his. So we never retry an undetermined result. We alert a human, who checks.

It's the same logic as for Mobile Money payments: the "I don't know" state exists, and treating it as a failure is the surest way to charge twice.

Dry run by default

The full submission path is proven in production. Yet by default, the agent fills in, reads back, and doesn't click. Production has to lift that guard explicitly.

Why? Because a fresh install (a developer machine, a test server) that started filing real requests at the port without anyone meaning to would be the worst possible default for this system. A default should be safe. Being useful is the configuration's job.

Same logic for concurrency: until we know how the portal reacts to two simultaneous sessions on the same account, one request at a time per account. We'll parallelise once we've measured it.

The spec is wrong, the site is right

Two discoveries contradicted the specification, and both would have sent drivers off for nothing.

The column that was supposed to hold the code doesn't. According to the spec, you just had to wait for a column to fill up to get the access code. The captures showed it held the request number, present from the moment of filing. The real signal arrived a quarter of an hour later, somewhere else: a status column switching to "authorised". Had we followed the document, every request would have been announced as approved the moment it was filed.

A rejection nobody had planned for. On top of the harbour master's refusal, the portal can return a refusal from another party. The spec didn't mention it. Without it, a driver would have set off for an entry that would never come.

The lesson: when you automate a system you don't control, the only reliable specification is the system itself. Real captures, anonymised, became the test fixtures.

Copy the typos

A rule that makes people smile, then adopt it: in our database, each driver's name and each vehicle's plate are copied character for character from the portal. Typos included.

If the portal spells a name with an extra letter, so do we. It's the only way to compare our screens with its form without interpreting. Fixing the spelling on our side would create a gap to reconcile on every request. Copying turns every divergence into a visible anomaly.

Flag, don't block

Container numbers follow the ISO 6346 standard, with a check digit. The app recomputes it as the driver types. When it doesn't match, it suggests a correction, without refusing. The driver has the container in front of him; we don't.

Two surprises while writing the tests:

  • all six sample numbers in the mockups failed the check, including the one in the specification;
  • the check is strong on digits, weak on letters. Depending on the number, 22 to 31 of the 325 possible letter pairs give the same check digit at a given position. A typo in the prefix can therefore slip through.

The check digit is a safety net, not proof. The real safeguard remains verification at the terminal.

The human takes over, without shame

When the agent stops, supervisors take over. Two outcomes are designed in, and neither is a failure:

  • abandoned: supervisors take the request away from the agent. On screen it's grey, not red, because nobody failed at anything;
  • manual takeover: supervisors file the request on the portal themselves and enter the code they get, which flows back to the driver with no retyping.

That colour detail matters. An interface that shows every cautious stop in red teaches its users to switch the agent off.

The takeaway

Automating a website without an API is doable, and often the only option. But the code that fills in the form is the smallest piece of the system. What matters:

  1. Define real success before writing a line.
  2. Read over HTTP, write with a browser, and isolate everything you know about the site in one module.
  3. Recognise the page before touching it, read back before clicking, capture at the moment of the click.
  4. Three verdicts: never retry what you can't qualify.
  5. A safe default: dry run until told otherwise.
  6. The site is the truth, not the document describing it.
  7. An agent stopping isn't a fault: design the human takeover as a normal path.

An agent that never stops isn't reliable. It's just lucky, for now.


The MOVELT project is described here. Got a process that relies on typing into a portal with no API? Message me: we'll start by working out what the automation must never do.