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

  1. Opens a headless Chromium browser in the cloud sandbox.
  2. Signs in to the portal with the username and password from the linked credential.
  3. Opens the report page for the requested month and clicks the export button.
  4. Reads the downloaded CSV with Python's standard library.
  5. 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 Portal and choose Custom HTTP headers (under Advanced). Add two rows, username and password. The script reads them as CREDENTIAL_VENDOR_PORTAL_USERNAME and CREDENTIAL_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_label and get_by_role with 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

  1. Save the draft and select Test.
  2. 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.
  3. 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_number for the portal's format.
  4. If it fails, read Stdout for the last stage reached and Stderr for the error, adjust the labels, and select Run again.
  5. 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 month out 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 Portal credential for its admin to fill in.

Variations

  • Authenticator codes: add a totp_seed row 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%.