What should you log when an automation moves money?
Enough to answer one question later without guessing, which is whether a given operation happened once, twice, or not at all. That means logging the identifier you sent, the response you received, and the parameters you sent with it. A timestamp and a success flag will not answer it.
This is the category of automation where sloppy logging costs real money rather than embarrassment. A duplicated charge, a double refund or a missed invoice all end up in a conversation with a customer, and the quality of that conversation depends entirely on what you wrote down at the time.
Payment providers have thought about this harder than most, and the mechanism they give you is the thing to build your logging around.
Why is a timestamp and a status not enough?
Because the ambiguous cases are exactly the ones a status does not cover. A request that timed out has no status. A request that returned an error may or may not have executed. Your log says failed, the customer says they were charged, and you have no way to reconcile those two statements.
The failure mode I see most often is an automation that retried after a timeout and created a second operation. The log shows two attempts and one success, which looks correct, while the provider shows two completed operations. Nothing in your own records would ever reveal that, because your records only describe what you tried.
What resolves it is logging something that identifies the operation rather than the attempt. Then two log lines that share that identifier are two attempts at one thing, and two log lines with different identifiers are genuinely two things. That distinction is the whole ballgame.
What is an idempotency key, and why should it be in your log?
It is a unique value your client generates so the provider can recognise retries of the same request. Stripe's API documentation describes it as supporting idempotency for safely retrying requests without accidentally performing the same operation twice, so you can repeat a request after a connection error without creating a second object.
Stripe explains the mechanism plainly. It saves the resulting status code and body of the first request made for any given idempotency key, regardless of whether it succeeds or fails, and subsequent requests with the same key return the same result, including 500 errors. Your retry does not re-execute anything, it replays an answer.
That makes the key the single most valuable thing in your log. It is the identifier that ties every attempt to one intended operation, on both sides of the integration. If you log nothing else beyond the basics, log the key you sent.
How should you generate the key, and what should it not contain?
Randomly, and it should not contain anything about the customer. Stripe says how you create unique keys is up to you but suggests version 4 UUIDs, or another random string with enough entropy to avoid collisions, and that idempotency keys are up to 255 characters long.
It also gives a specific warning worth repeating. Stripe advises avoiding sensitive data such as email addresses or personal identifiers as idempotency keys. That is easy advice to breach accidentally, because building a key from a customer email and a timestamp feels tidy and guarantees uniqueness.
Do not do it. Those keys end up in logs, in error messages, in monitoring tools and in support tickets, which quietly spreads personal data into systems that were never scoped to hold it. A random key with the customer reference stored separately in your own record gives you the same traceability with none of that.
What does the provider remember, and for how long?
Not forever, which changes how you think about late retries. Stripe says keys can be removed from the system automatically after they are at least 24 hours old, and that it generates a new request if a key is reused after the original is pruned.
Read that carefully, because it inverts the safety property you were relying on. Inside that window, resending is safe and returns the stored result. Outside it, the same key is simply an unrecognised key, and the request executes as a fresh operation. A retry queue that drains slowly after an outage can produce duplicates precisely because it retried too late.
The practical rule is that idempotency protects you against retries measured in seconds and minutes, not in days. Anything older than that window should go to a human queue for reconciliation rather than being retried automatically, and your log needs the original timestamp so you can tell which case you are in.
What happens if you reuse a key with different parameters?
You get an error rather than a surprise, which is the correct design. Stripe says its idempotency layer compares incoming parameters to those of the original request and errors if they are not the same, specifically to prevent accidental misuse.
This matters for how you build retries. If your automation recalculates anything between attempts, such as a timestamp inside the payload or an amount derived from a changing source, the second attempt is not the same request any more and it will be rejected. The rejection is protecting you, and it will look like a bug.
The fix is to build the complete payload once, store it with the key, and retry that exact stored payload rather than regenerating it. That is also why the parameters belong in your log. When a retry errors on mismatch, the only way to find out what changed is to compare the two payloads, and you can only do that if you kept the first one.
Which requests can you safely retry?
Fewer than you might assume, and the provider tells you which. Stripe says results are saved only after execution of an endpoint begins, so if incoming parameters fail validation or the request conflicts with another executing concurrently, no idempotent result is saved and those requests can be retried.
It is also explicit about scope. Stripe says all POST requests accept idempotency keys, and that you should not send them on GET and DELETE requests because it has no effect, since those are idempotent by definition. Sending keys everywhere is harmless noise, but it suggests a mental model that will mislead you elsewhere.
Log the distinction. A retryable error and a non-retryable one should look different in your records, because six months later the person reading the log will not remember which category a particular response code fell into, and the log is the only thing that will tell them.
What should never go in the log?
Full card details, full credentials, and complete customer records. The log is the least protected place in your stack. It gets copied into monitoring tools, pasted into tickets, and read by people who would never be given access to the payment system itself.
Log references rather than values. A customer identifier rather than an email, an amount and currency rather than a payment instrument, a key rather than the data the key was derived from. That gives you everything you need to reconstruct an incident while leaving the sensitive material where it belongs.
This connects directly to the permissions question, because a log with too much in it is the same failure as a credential with too much access. Both are convenience decisions made at build time that become somebody else's problem later, which is the argument in what an automation should never be allowed to do.
How long should you keep these records?
Longer than the disputes. Financial operations get questioned on timescales that have nothing to do with your engineering retention policy, and a log that rolls off after thirty days is useless for a chargeback raised in month three. Decide the retention period from the dispute window, not from the disk cost.
Separate the two kinds of record while you are at it. Operational logs for debugging can be noisy and short-lived. Financial event records should be structured, minimal and kept, and they are a different artefact with a different lifetime even if they start from the same event.
Write the retention decision into the runbook so it survives the person who made it. That is the same document that should carry your retry rules and your escalation path, and I have argued before for writing the runbook before you ship rather than after the first incident.
What should you do next?
Find one automation in your stack that creates a financial operation and check whether its log contains an idempotency key. If it does not, that is the first thing to add, and it is usually a few lines rather than a project.
Then check what your retry logic does after an hour, after a day, and after a week. If it retries identically at all three, you have a duplicate waiting to happen at whichever point your provider's key retention ends. Move the long tail to a human queue instead, which is the same reasoning behind choosing between a schedule and a trigger in the first place.
Finally, read your provider's own idempotency documentation rather than assuming it matches Stripe's, because these details differ between providers and the differences are exactly where the money goes missing. If you want a second pair of eyes on an automation that touches payments before it goes live, reach out.
Get found, cited and the back office automated
Let's make your site the source AI engines quote and wire up the systems behind it.
Read more blogs
Let's get your website found and cited by AI
Tell me what you're working on, whether AI search is skipping your product, your back office is buried in manual work, or you need a build that does both.