I'd have the browser create the operation ID and save it before the request goes out, then make Retry a lookup instead of a resubmit. Retry first asks the server for the job under that ID. If one exists, the user sees its status and nothing new is charged. Only when the server has never seen the ID does Retry submit again, with the same ID, and a unique constraint on that column covers the case where the first request is still landing.
Key expiry is where I'd part ways with the Stripe model. Its v1 docs say keys can be removed once they're at least 24 hours old, and a request reusing a pruned key is treated as new. That works for payment retries that happen within seconds. A photographer who closes the laptop and clicks Retry the next morning would get a second paid batch. For image jobs I'd keep the operation ID on the job record for as long as the job or its credit reservation exists, and if an ID has expired, return a clear error rather than quietly starting a new job.