Assigning Operation IDs When Your API Returns a 202
Using the client's ID as the operation ID will eventually point one customer at another customer's job. Key uniqueness on the client plus their value, and enforce authorization on every status call.
Use the order ID your client sent you as the operation ID, and eventually you will hand one customer a status URL that points at another customer’s job.
That is the failure mode worth understanding first, because everything else about this decision follows from it.
Once your API has accepted a request and returned a 202 instead of a 200, the next decision is how that request gets identified. It looks like plumbing. It is actually where uniqueness, tenant isolation, and duplicate detection all meet, and the URL you hand back is that decision made visible to the outside world.
There are three approaches. Which one fits depends on the shape of the payload, the intent of the service, and who is calling it.
Using the ID the client supplied
The simplest approach, and the one most developers reach for, is to use an
identifier that came in with the request. An OrderId, or an idempotency key
the client generated to assert this request is unique.
Under the right conditions it is solid. If your only caller is an internal
system, and the request references records that already exist in your database,
that OrderId is unique and you are done.
It falls apart the moment you have more than one client.
Two callers, two separate systems, two independent ID sequences. Nothing stops both of them sending you order 4471. If your uniqueness check is the client’s value alone, you now have two different requests that your system believes are the same request.
The first consequence is the leak. If you are doing duplicate detection, and you should be, the second caller gets handed a status URL for the first caller’s job. If your authorization is solid they hit a 403 on a resource that is none of their business. If it is not, they are reading another customer’s data, and you built that path yourself.
The second consequence is quieter and just as bad: the second caller’s request never gets processed at all. They got a 202. They have a URL. Everything looks fine from where they are standing.
The fix is not complicated. Track uniqueness on the combination of the client’s value and your internal identifier for that client. Duplicate detection keeps working, and tenant separation becomes a property of the key rather than something you remember to check later.
Generating your own UUID
The second approach is to mint a fresh internal ID for every request that comes in. Uniqueness is solved by construction, and you can no longer point one client at another client’s job.
What you lose is the ability to recognize a repeat. If every request gets a new ID the moment it arrives, every request looks new. The client hits a network timeout, their retry logic fires, and the same request gets processed twice.
If something downstream makes that safe, fine. If nothing does, and the endpoint charges a credit card, you just charged it twice.
The hybrid, and what it costs
What I use: generate an internal ID for every request, but only after establishing the request is unique.
Uniqueness is tracked on the pair of my internal client ID and whatever uniqueness value the client supplied. A lookup table answers one question: has this client sent me this before? If they have, they get back a path to the status of the existing job rather than a second job.
The decision to make there is what status code that response carries. A 409
Conflict says plainly that this was a repeat. A 202 with a marker, something
like an IsDuplicate: true header or a field in the body, treats the replay as
success and tells them why.
The prevailing convention for idempotent replay is the second one: return the original response rather than an error, on the reasoning that a retry behaving identically to the first call is the entire point of an idempotency key. It also means the client does not have to branch on a status code to handle the case they were never worried about. Either way, the response has to carry the status path, and the caller has to be able to tell a replay from a fresh accept.
When the client supplies no uniqueness value at all and duplicate prevention matters, the burden falls on the API. A SHA256 of the payload works, but only over a canonical serialization. Two semantically identical payloads with different key ordering or whitespace hash differently, and your duplicate detection silently stops detecting anything.
Do not hand out your internal IDs
Once a request is validated, found unique, and recorded, it has an internal ID. The client never sees it. Exposing internal identifiers is a security risk and usually just one more opaque value the caller has to store for no benefit.
The path I return looks like this:
/jobstatus/{client-identifier}/{client-supplied-id}
The client-identifier is a key issued to that client, not the internal client
ID I store against it.
That shape does two things. It keeps internal IDs inside the boundary where they belong. And it gives the caller a predictable URL, which means they can build status checking once and never track a response ID from my system at all. They already know both halves of the path, because they supplied one and were issued the other.
The tradeoff you are accepting is that this URL is guessable, by design. That is the benefit and the risk in the same sentence. A predictable path carries no security whatsoever, so authorization has to be enforced on every status call, every time. This runs against the instinct that a Location header points at something opaque and unguessable. It is a fine trade, but only if it is a decision rather than an accident.
One loose end worth closing deliberately: when the client supplied no uniqueness value and you fall back to the payload hash, the predictable path stops being predictable to them. They either store the hash you returned, which is the response-ID tracking the design was meant to avoid, or they recompute it, which means your canonicalization rules become published contract. Pick one and document it. Leaving it implicit is how a client ends up with neither.
The point
Assigning an ID looks like an implementation detail and behaves like an architectural decision. It determines whether duplicate requests are caught, whether one client can ever see another client’s work, and how much of your internals leak into a URL you cannot take back once someone builds against it. Worth deciding on purpose, at the point the request arrives.