no account · no credentials · nothing behind a login
A thousand engineers can send you the same CV. I would rather show you I can read a payload. So I spent a weekend inside your public API guide, your OpenAPI specification and your login page, and wrote down everything that would bite an integrator.
Thirty-three findings. Here is the first, about twenty minutes in.
developers.qashio.com, Integration-Critical Notes
Neither field is ever returned. Your declared response type FindAllResErpTransactionObjectDto lists 54 properties and includes neither. billingCurrency is at least an accepted query filter, so the concept exists server side. billingAmount appears nowhere in the API at all. This is the number your customers post to a general ledger.
Four sources, all public. No account, and I did not go looking for one.
developers.qashio.comerp.qashio.com/api/docs, linked publicly from that guideNo requests hit your API. Nothing probed, fuzzed or load-tested. You are a regulated fintech, and an applicant who treats your production systems as a sandbox has answered a different question than the one on your job ad.
One finding is deliberately not here. It concerns how request scope is established. It is a design question rather than something I proved, and proving it would have meant crossing the line above, so it went to you privately and phrased as a question.
The missing fields are not the problem. What you get instead is.
erp.qashio.com/api/docs, GET /erp-transactions, response example
"amount": 102, "currency": "USD", "transactionAmount": 102.5, "transactionCurrency": "USD", "feeCharges": 2.75 // no currency field, no inclusion rule
Same currency, two values, neither documented, and no rule for which one wins. An integrator follows your guide to a field that is not there, then guesses between two that are, in a market where VAT recovery depends on the answer.
Whether this is a documentation defect or a settlement modelling gap turns on whether your amounts update on clearing. That needs a real account to watch a transaction cross from authorisation to settlement, so I logged it as an open question rather than picking whichever version sounded worse.
Chart of accounts, suppliers, tax rates and bank accounts all accept updatedAtFrom and updatedAtTo. That is master data. It barely moves.
Transactions get clearedAtFrom and transactionTimeFrom, both of which timestamp when the money moved, and no updatedAt filter at all. The response hands you updatedAt and modifiedDate. The timestamps are right there. You just cannot query them.
Transaction records move constantly after settlement: approvals land, receipts get attached, someone edits a memo or a segment, sync status advances. None of that touches clearedAt or transactionTime, so none of it is findable without re-reading everything. At 30 requests a minute and 500 rows a page, a customer with a hundred thousand transactions needs around two hundred sequential calls. Roughly seven minutes, across a collection shifting underneath the cursor the whole time.
Offset pagination over a moving collection breaks in both directions, so I tested both rather than asserting either:
verify.mjs, run against the published example payloads, 10/10 passing
insert mid-scan t9 returned in both pages posted to the ledger twice delete mid-scan t10 never returned silently missing from the ledger keyset cursor no duplicate, no gap insert appears on a later page
A full scan is the only sync mode on offer, so this is the normal condition of every sync a busy customer runs, not an edge case.
Ask in one casing, get back another. Send that same value straight back as a filter and you get zero rows, HTTP 200, no error. Which is the worst way anything can fail, because nothing tells you it happened.
Three of the four transaction enums break on the round trip and one works fine, so the rule is not learnable from a single test. Your integration guide then lists the same enum in a third casing, CARD_LOADING, with one entry written FORCED DEBIT, space and all.
Google's field data says app.qashio.com/login fails its Core Web Vitals assessment. The cause has no ambiguity in it.
First byte lands at 0.7 seconds. Nothing paints until 2.5. That 1.8-second gap is the entire story. Your server is fine, your Dubai edge is fine, and all of the delay is bundle download, parse and execute against an empty HTML shell. Largest paint trails first paint by half a second, so it needs no separate fix.
The bundle is 8.72 MB across 199 resources, 64.6% of it unused on first paint.
executed on first paintdownloaded, parsed, never executed
Your support chat widget outweighs your product. Underneath it sit three chunks with no business on a login screen: a data grid at 459 KB, 89% unused; an icon set at 392 KB, 97% unused; a page shell at 333 KB, 96% unused. That is 1.21 MB of authenticated application code shipped to every visitor who never logs in, for a form with two inputs and a button.
The 335 ms of interaction latency on that form is not your application logic. It is main-thread contention, which brings us to everything else loading alongside it.
The cookies transmitted to your login page identify thirteen distinct third-party products: Meta, Google Analytics across three properties, Microsoft Clarity, Hotjar, HubSpot, Google Ads, Intercom, ContentSquare, Reddit Ads, Microsoft Bing UET, PostHog across two projects, Leadfeeder, and one unidentified vendor writing a __ps_ cookie family. Google Sign-In is there too, but that is authentication rather than tracking, so it is out of the count. They are scoped to the parent domain, so campaign names, keyword terms and click identifiers ride along onto the app origin.
Four of them offer DOM-level session replay, recording rendered content and form input. This is the origin that, one screen later, shows your customers' cardholder names, emails, masked card numbers, balances and transaction detail.
There is no Content-Security-Policy header on that origin. You set HSTS, you set nosniff, you set X-Frame-Options, which CSP's frame-ancestors has superseded, and you set X-XSS-Protection: 1; mode=block, which current guidance says to turn off rather than on. The one policy that would constrain what those scripts may read, and where they may send it, is the one that is missing.
Exposure surface, not incident. Whether you have masked or disabled replay after login I cannot see from out here, and I did not go looking. What I can see is that the control which would enforce it is not there. Only vendor configuration is, and configuration is one dashboard toggle from being something else.
All thirty-three, with source and status attached to each, so you can check any of them against your own systems.
Select any row to open its evidence, impact and fix.
A study that only attacks is not an audit. Plenty of what you have built is done well, and some of it better than most teams manage.
Not findings. This is the edge of what public evidence establishes, written down so you can see exactly where I stopped.
You already ship AI in production. Receipt matching, WhatsApp receipt collection, and an API that backs it up: receipts comes back as an array of URLs, and your guide documents vendorTrn as captured from the user-uploaded invoice. So a supplier's VAT registration number is coming out of a model today.
What the API does not return is any confidence score, any flag separating a value a model extracted from one a human typed, or any review state. From an integrator's side those are indistinguishable. One of them feeds UAE FTA compliance.
That is not a bug. It is the shape of the work your job ad describes: provenance on every extracted field, a confidence threshold routing low-scoring extractions to human review instead of straight to the ledger, and an eval set built from real receipts so quality regressions surface before a customer's VAT return does.
Thirty-three findings, eight things working, four open questions. All of it from your documentation and your login screen, which is roughly a thousandth of what is visible from inside.