Syncing Lysted → SkyBox Through Slack: When an HTML Email Is the Only API
The final post in my series on an automation hub for a ticket-resale business. This is a very real-world integration story: a sales platform with no webhooks and no usable order API — the only thing you get is an HTML "TICKETS SOLD" notification email. Plus the settlement-reconciliation pipeline that records payments onto invoices automatically.
Context
The team sells tickets on Lysted, but inventory and invoices are managed centrally in SkyBox (VividSeats' POS). Every Lysted sale has to be recorded in SkyBox: matched to the right event, section, row, and seats in inventory, with the invoice reflecting the correct payout. Lysted doesn't fire webhooks. What the team did have: "tickets sold" emails already being forwarded into a Slack channel.
So: Slack is the message queue, and the HTML email is the API.
Pipeline 1: Recording sales
Slack conversations.history (bot token)
→ store messages in MySQL (idempotent — nothing processed twice)
→ download attachments (private files need an auth header)
→ parse the HTML with HtmlAgilityPack
→ extract event / venue / section / row / seats / payout
→ match against SkyBox inventory + invoicesThe scraping story: when the email template betrays you
Lysted's emails have no decent classes or IDs to anchor on — the data columns can only be distinguished by each <div>'s inline style. Initially I matched the style strings exactly with XPath. It ran fine for months, and then one day the pipeline went silent and stopped catching sales.
The cause: Lysted changed their template — identical content, but the styles gained whitespace after the semicolons (margin:0;padding:0 → margin: 0; padding: 0). Exact matching fell apart.
The fix: normalize the whitespace in the style attribute before comparing, treating two style strings as equal if they differ only in spacing. Scraping lesson #1: never match machine-generated output verbatim — always normalize first.
The date-parsing story: "Oct 10th 2026, 11:59pm"
.NET has no format specifier that understands ordinal suffixes like st/nd/rd/th. The clean solution: regex-strip the ordinal suffix first, then DateTime.TryParseExact with a fixed format. Small, but it's exactly the kind of bug that silently corrupts data if you don't catch it.
Pipeline 2: Settlement reconciliation — the job that touches money
Periodically, Lysted posts a remit CSV (the list of orders being paid out) into another Slack channel. This job:
- Downloads the
lysted-remit-*.csvfile and parses it with CsvHelper in break-resistant mode:HeaderValidated = null,MissingFieldFound = null, a custom ClassMap, and a predicate that drops the footer/summary rows (starting with#) that Lysted appends to the file. - Logs checksum totals for payout / commission / profit so they can be sanity-checked against the portal quickly.
- For each row: look up the SkyBox invoice by external reference → if already
PAID, skip it (idempotent — reruns never write duplicate payments) → otherwise fetch the full invoice, inject aPayment(ACH method), and PUT it back to the SkyBox API. - Every outcome (success/failure/skip) is written to Google Sheets. A small trick: use
InsertDimensionRequestto insert new rows at row 2 instead of appending at the bottom — the finance team opens the sheet and the freshest data is right there, no scrolling.
Pipeline 3: The daily report
A rollup job reads the log sheet back, filters to yesterday's rows, tallies success/failed counts, and posts a Slack Block Kit summary to the operations channel. The loop is closed: data comes in from Slack, results are reported back to Slack — the team never needs to open another tool to know whether the system is alive.
Principles that emerged
- Idempotency comes first when a job touches money: check the
PAIDstatus before writing a payment, persist processed messages in the DB — running it 10 times must produce the same result as running it once. - Durable scraping = normalize everything before comparing: whitespace, ordinal suffixes, encodings. The email template will change; the only question is when.
- Use the infrastructure you already have: a Slack channel as a queue, Google Sheets as a dashboard, email as an event source. Not every problem needs Kafka.
- Fail loudly: a CSV row that won't parse or an invoice that won't match must never be swallowed silently — everything goes to the sheet and to Slack so a human can handle the remainder.
That wraps up the series. If you're building automation for business operations and want to talk through any of the patterns in this series, drop a comment.
Tech stack for this post: Slack Web API (conversations.history, Block Kit), HtmlAgilityPack, CsvHelper, RestSharp, SkyBox (VividSeats) API, Google Sheets API v4, MySQL.
Comments
Post a Comment