Use a Playwright action to sign in to a portal that has no API, export a report and summarise it for SuperIT
Automate a report you'd otherwise download by hand from a vendor portal with no API, such as a licence usage export, a telecoms bill or a monthly service report. A Playwright workflow action signs in, exports the file, reads it and gives the SuperIT agent the figures that matter. For MSPs and IT teams stuck with portals that only offer a download button.
| Runs in | Cloud sandbox (Playwright) |
| Uses | API key credential (portal username and password stored as custom headers) |
| Suggested approval | None (engineers and admins run it without approval), because it only reads |
| Category | Browser automation |
Test it with SuperIT before you rely on it
This is an example, not a ready-made action, and it hasn't been tested end to end. The portal address, labels and CSV columns are placeholders: every portal needs its own. Before you use it, work through it with SuperIT: ask SuperIT to review the script against your environment and requirements, and adjust it together. Then test it, as in Test it. Test in the action editor isn't a dry run: it runs the script for real.
Why use an action instead of asking SuperIT
SuperIT has no web browser. It can search the web, but it can't sign in to a portal, click through it or download a file. A Playwright action does that headlessly in the cloud sandbox, with the portal login stored as an encrypted credential, and returns the figures that matter from the downloaded report. If the information is public, or lives in Microsoft 365 (such as Microsoft 365 licence counts, which SuperIT reads through the Microsoft 365 connection), just ask SuperIT in chat instead.
When to use it
- "How many seats did we use on the phone system last month?"
- "Pull October's usage report from the vendor portal and tell me who's over their allowance."
- "What did the customer's connectivity bill come to this month?"
What the action does
- Opens a headless Chromium browser in the cloud sandbox.
- Signs in to the portal with the username and password from the linked credential.
- Opens the report page for the requested month and clicks the export button.
- Reads the downloaded CSV with Python's standard library.
- Returns totals and the top lines as text and a structured result. If a cost or usage value can't be read as a number, it says how many, so a total that's too low doesn't go unnoticed.
What it doesn't do:
- It doesn't return the downloaded file. The script reads the file in the sandbox and returns only the summary.
- It doesn't complete interactive sign-in challenges, such as a push notification or SMS code.
- It doesn't run on a device. Playwright actions run in the cloud sandbox only.
Before you begin
- A portal account for automation, ideally read-only and used only by SuperIT, so you can see its activity and revoke it without affecting a person.
- Multi-factor authentication. If the portal insists on it, use an authenticator-app (TOTP) factor and store its setup key too (see Variations). Some portals let you skip MFA for sign-ins from known IP addresses; SuperIT's sandbox uses fixed outbound IP addresses (see Network).
- A credential. Under Workflows → Credentials, select New, name it
Vendor Portaland choose Custom HTTP headers (under Advanced). Add two rows,usernameandpassword. The script reads them asCREDENTIAL_VENDOR_PORTAL_USERNAMEandCREDENTIAL_VENDOR_PORTAL_PASSWORD. Values are encrypted at rest. See Credentials. - Check the portal's terms allow automated access.
- Your role. Creating an action in chat needs the Admin role. Engineers can build it by hand under Workflows → Actions. See Approvals for who can run it.
Build it with SuperIT
Describe the clicks you'd make. SuperIT turns them into a Playwright script:
Create an action called "Download vendor usage report".
Purpose: sign in to https://portal.vendor.example, open Reports > Usage for a given month, click "Export CSV", and summarise the file.
Runtime: Playwright/Python in the cloud sandbox.
Input: month (string, optional, format YYYY-MM, default last month).
Credential: "Vendor Portal" (custom headers username and password).
Sign-in page: fields labelled "Email" and "Password", button "Sign in".
Summarise: total seats, total cost, and the 10 users with the highest usage. Never print the password.
No authorization policy: engineers and admins only, no approval.
SuperIT drafts the script, validates it and creates the action as a draft. Portals change, so expect to adjust labels after the first test. You review it, test it and publish it under Workflows → Actions.
Or build it yourself
| Name | Type | Required | Example |
|---|---|---|---|
month |
string | No (defaults to last month) | 2026-10 |
Set Runtime to Playwright/Python. SuperIT starts the browser for you and provides page, browser_context and input_context. Use await at the top level, and assign output to return a result. As in SuperIT's own scripts, main comes first and reads as the plan, each step is a small function below it, and the last line runs it: output must be assigned at the top level, not inside a function.
# Goal: sign in to the vendor portal, export one month's usage CSV and summarise it. Read-only.
import csv
import io
import os
from datetime import date, datetime, timedelta
from pathlib import Path
PORTAL = "https://portal.vendor.example"
async def main(month_input):
month = resolve_month(month_input)
username, password = read_credential()
await sign_in(username, password)
rows = await download_usage_rows(month)
summary = summarise(month, rows)
print_summary(summary)
return summary
def resolve_month(value):
month = (value or "").strip()
if not month: # default to last month, so a schedule needs no input
first_of_this_month = date.today().replace(day=1)
month = (first_of_this_month - timedelta(days=1)).strftime("%Y-%m")
try:
# strptime also rejects months such as 2026-13, which a pattern check lets through.
return datetime.strptime(month, "%Y-%m").strftime("%Y-%m")
except ValueError:
raise RuntimeError("Input 'month' must be a real month, such as 2026-10.") from None
def read_credential():
username = os.environ.get("CREDENTIAL_VENDOR_PORTAL_USERNAME")
password = os.environ.get("CREDENTIAL_VENDOR_PORTAL_PASSWORD")
if not username or not password:
raise RuntimeError("The 'Vendor Portal' credential is missing username or password.")
return username, password
async def sign_in(username, password):
await page.goto(f"{PORTAL}/login", wait_until="domcontentloaded")
await page.get_by_label("Email").fill(username)
await page.get_by_label("Password").fill(password)
await page.get_by_role("button", name="Sign in").click()
try:
await page.wait_for_url("**/dashboard**", timeout=30000)
except Exception:
raise RuntimeError("Sign-in didn't reach the dashboard. Check the credential, or whether the portal asked for MFA.")
print("Signed in.")
async def download_usage_rows(month):
await page.goto(f"{PORTAL}/reports/usage?month={month}", wait_until="domcontentloaded")
async with page.expect_download(timeout=60000) as download_info:
await page.get_by_role("button", name="Export CSV").click()
download = await download_info.value
text = Path(await download.path()).read_text(encoding="utf-8-sig")
rows = list(csv.DictReader(io.StringIO(text)))
if not rows:
raise RuntimeError(f"The {month} report downloaded but has no rows.")
print(f"Downloaded {len(rows)} rows.")
return rows
def summarise(month, rows):
costs = [parse_number(r.get("Cost")) for r in rows]
unreadable = sum(1 for c in costs if c is None)
total_cost = sum(c for c in costs if c is not None)
top = sorted(rows, key=lambda r: parse_number(r.get("Usage")) or 0.0, reverse=True)[:10]
return {"month": month, "seats": len(rows), "total_cost": round(total_cost, 2),
"unreadable_costs": unreadable,
"top_users": [{"user": r.get("User"), "usage": r.get("Usage"), "cost": r.get("Cost")} for r in top]}
def parse_number(value):
# None, not 0, for a value that isn't a number, so the summary can count and report it.
text = str(value or "").replace(",", "").replace("£", "").replace("$", "").strip()
if not text:
return None
try:
return float(text)
except ValueError:
return None
def print_summary(summary):
print(f"=== USAGE REPORT {summary['month']} ===")
print(f"Seats : {summary['seats']}")
print(f"Total cost : {summary['total_cost']:.2f}")
if summary["unreadable_costs"]:
print(f"WARNING: {summary['unreadable_costs']} row(s) had a cost that isn't a number and aren't in the total.")
for u in summary["top_users"]:
print(f"- {u['user']}: usage {u['usage']}, cost {u['cost']}")
output = await main(input_context.get("month"))
Tips for portal scripts:
- Prefer
get_by_labelandget_by_rolewith the text you see on screen. They survive layout changes better than CSS selectors. - Use
print()after each stage ("Signed in.", "Report page loaded."), so a failed run shows how far it got. - Raise an error with a clear message when something unexpected happens. A run that raises is marked failed.
Test it
- Save the draft and select Test.
- Test is not a dry run: it skips authorization and approvals and uses the real credential and network access, so it really signs in to the portal. Test runs aren't billed.
- Run it for last month and compare the totals with the portal. If Stdout shows a warning about costs that aren't numbers, adjust
parse_numberfor the portal's format. - If it fails, read Stdout for the last stage reached and Stderr for the error, adjust the labels, and select Run again.
- Select Publish.
Use it
- In chat: "Pull last month's usage from the vendor portal." SuperIT works out the month, runs the action and summarises the result.
- Every month: ask SuperIT to schedule it every 30 days. Schedules repeat every N elapsed days, not on a calendar date. Leave
monthout and the script reports on last month. - Managed teams: if each customer has its own portal login, use Publish to teams. Each managed team gets a read-only copy and a blank
Vendor Portalcredential for its admin to fill in.
Variations
-
Authenticator codes: add a
totp_seedrow to the credential and generate the code with the standard library:import base64, hashlib, hmac, struct, time seed = os.environ["CREDENTIAL_VENDOR_PORTAL_TOTP_SEED"].replace(" ", "").upper() key = base64.b32decode(seed + "=" * (-len(seed) % 8)) digest = hmac.new(key, struct.pack(">Q", int(time.time()) // 30), hashlib.sha1).digest() offset = digest[-1] & 0x0F code = f"{(struct.unpack('>I', digest[offset:offset + 4])[0] & 0x7FFFFFFF) % 1000000:06d}"Storing the seed means SuperIT holds both factors for that account. Limit the account's rights accordingly. - Read a table instead of a file: if the portal shows the data on screen, read it with
page.locator("table tr").all_inner_texts()and skip the download. - Invoices: download the latest invoice and compare its total with last month's, flagging changes over 10%.