Retrying a write on this API is always safe, provided you resend the same Idempotency-Key. That is the whole reason the header is mandatory.

What to retry

Never retry a 4xx other than 429 and idempotency_key_in_progress. The request will fail identically forever, so a retry loop is load with no chance of success.
Because client faults are never returned as 5xx on this API, the split above is reliable: a 5xx really does mean our side, and retrying it is reasonable.

The timeout case

This is the case retries exist for, and the one people get wrong. When a request times out you do not know whether the server acted. The connection died, but the write may well have committed. Without an idempotency key your options are to retry and risk a duplicate order, or not retry and risk losing it. With one, retry. If the original committed, you get its stored response back and nothing happens twice. If it never arrived, it runs now.
Generate the key before the first attempt and hold it for every retry of that operation. A key generated inside the retry loop is a new key each time, which reinstates exactly the duplicate-write risk you were trying to avoid.

Backing off

1

Honour Retry-After when it is present

On a 429 the server tells you exactly how long to wait. Use that number rather than your own schedule.
2

Otherwise back off exponentially

Roughly 1s, 2s, 4s, 8s, 16s. A fixed short delay against an overloaded service adds load at the worst moment.
3

Add jitter

Randomise each delay by up to a few hundred milliseconds. Without it, every client that failed at the same instant retries at the same instant, and the recovering service is hit by a synchronised wave.
4

Cap the attempts

Five is usually right. Beyond that you are not recovering from a blip, and the failure should surface to a human or a dead letter queue.

A worked implementation

Reads

GET requests are naturally safe to retry and take no idempotency key. Apply the same backoff rules. If a paged walk fails mid-way, retry the failed page with the same after cursor. You do not need to restart the walk.

When to stop and alert

Retries hide transient failures, which is their job. They should not hide a persistent one. If an operation exhausts its attempts, surface it: a log at error level with the x-request-id, a dead letter queue, or an alert. Silently dropping a failed write after five attempts is how an integration ends up quietly missing orders.