The ticket pattern is a good fit, and the HMAC plus allowlist section is the part I'd push hardest. One gap in the sample is that webhook_url is still an input the model supplies. Even with an allowlist, it is safer to have the server resolve the callback from the authenticated client or session, so the model never chooses where results go. I'd also dedupe on a hash of (user_id, tool_name, normalized input) and return the existing ticket, since a retrying agent otherwise queues duplicate 500MB exports. In the job, catching \Exception and writing 'failed' hides errors from the queue, so $tries and backoff never apply. Separating 'export failed' from 'webhook delivery failed' would let you retry delivery without redoing the four minute work.
How would you handle a ticket whose worker dies mid-run and leaves it in 'pending' forever, with a lease or heartbeat, or a sweeper job?