What the < actually tells you
The error message is literal. JSON.parse() read the first character of the response, found <, and stopped. The < is the opening of <!DOCTYPE html> — the server sent an HTML page instead of a JSON payload. That HTML is almost always a 401 login redirect, a 403 forbidden page, a 404 not-found page, or a Cloudflare/WAF interstitial. Your code never sees any of that because, by default, UrlFetchApp.fetch() throws on non-2xx responses and swallows the body.
The first time I hit this, I spent twenty minutes second-guessing my JSON.stringify() call on the request side. The response body was the actual clue the whole time — I just had no way to read it.