Conversation Resume Identity Verification Automation: A Parameter That Was Documented and Ignored

A parameter that exists in the type system is not the same as a parameter that exists in the code
Adding a field to an API's documented input type is the easy, visible half of building a feature — the type checker enforces that callers may pass it, and nothing enforces that the receiving code actually does anything with the value once it arrives. When the two halves drift apart, the result is not a crash or a visible error; it is a parameter that behaves exactly as if it were optional and ignorable, silently, for as long as nobody specifically checks whether the value made it all the way through to the logic that was supposed to consume it.
How the underlying problem shows up before you fix it
A documented request parameter is copied into an internal field (metadata, a log context, an audit record) but a direct search of the codebase shows that field is written in exactly one place and read in zero.
Asking an API to resume, continue, or reference a specific existing resource by id instead produces a NEW resource, or a different existing one, with no error indicating the requested id was not honored.
The defect is invisible to a caller who never inspects which specific resource id came back — the response still looks entirely successful, just attached to the wrong underlying record.
An integration built against the documented parameter accumulates duplicate or fragmented history for what should be one continuous interaction, without an obvious single cause.
Fixing the gap naively — simply making the ignored parameter effective — introduces a NEW risk if nothing verifies that the caller is actually entitled to the resource being requested.
How a silently ignored parameter became a verified resume, with one refusal for every kind of mismatch
Centriu Synapse's public messaging endpoint accepts a `conversationId` field specifically so an integrator that already has an ongoing conversation with a customer can attach a new inbound message to that exact conversation, rather than the handler deciding on its own which conversation (or whether a new one) applies. Before this fix, the value arriving in that field was copied into `metadata.requestedConversationId` — and a direct search of the entire repository confirmed that specific field name appears in exactly one place: the line that writes it. Nothing downstream ever read it. The handler's actual conversation logic always ran its own, separate find-or-create-the-open-conversation path, completely independent of whatever conversationId the caller had explicitly asked for.
The practical consequence is a specific, quiet kind of data fragmentation: an integrator that believed it was continuing one particular conversation — because it deliberately supplied that conversation's own id — was, without any indication, having its message attached to whatever conversation the handler's own default logic happened to find or create instead. No error surfaced, because from the handler's point of view nothing had gone wrong; it had successfully done the thing it always does, just not the thing the caller had explicitly asked for.
The fix makes the parameter genuinely load-bearing, and does so carefully rather than naively. When `conversationId` is present, the handler now calls a dedicated resolution function that proves the requested conversation actually belongs to the calling API credential's own client before using it — checking that the conversation exists, that it belongs to the right tenant scope, that it belongs to the right customer record, and that it is not already closed. Only a conversation that passes every one of those checks is resumed exactly as requested; when the parameter is absent entirely, the historical find-or-create behavior is preserved unchanged, so existing integrations that never used the parameter see no change at all.
A request naming a conversation that fails any of those checks does not silently fall back to opening a different conversation — the specific failure that made the naive form of this fix newly risky. Instead, the handler raises a dedicated, typed error carrying an internal-only reason (not found, wrong tenant, wrong customer, or closed), and the route deliberately maps every one of those four distinct internal reasons to the exact same public response: a single 404 with a single message, "conversationId não encontrado para esta credencial." The route's own comment states the reasoning directly: distinguishing "this conversation exists but isn't yours" from "this conversation doesn't exist at all" would turn the endpoint into a tool an integrator (or anyone holding a valid API credential) could use to probe which conversation ids are real, one guess at a time. The specific reason is still recorded — but only in the internal log, never in the response a caller receives.
What is actually built today
Supplying a conversationId that genuinely belongs to the calling credential's own client resumes that exact conversation — proven, not assumed, before it is used.
Four distinct failure conditions (not found, wrong tenant, wrong customer, closed) are each detected internally and individually logged, but collapsed into one identical public refusal — a single 404 — so the endpoint cannot be used to distinguish which specific reason applied.
Omitting the parameter entirely preserves the exact prior behavior (find or create the client's open conversation) with no change — this fix is purely additive for callers who never used the field.
A refusal is raised as a genuine, typed error rather than a silent fallback to a different conversation — the historic defect (asking for one conversation, silently getting a different one) cannot recur through this path.
The specific internal reason for a refusal reaches the application's own logs, tagged with the request's trace id, for legitimate operational debugging — without ever reaching the API response itself.
Asking for one conversation, quietly getting another (illustrative framing of the actual measured finding)
An integrator's own system stores a specific conversation id after a first exchange, and on a follow-up message deliberately sends that same id to keep the interaction continuous. Before the fix, the handler accepts the message, ignores the id entirely, and finds-or-creates a different conversation — the integrator's own system now believes it is continuing one thread while Synapse has silently attached the reply to another. After the fix, the same request either genuinely resumes that exact conversation, proven to belong to the caller, or receives an explicit, honest refusal — never a substitution the caller has no way to detect.
What changes operationally
Centriu Synapse's messaging API now genuinely honors a request to resume a specific conversation, only after verifying it belongs to the calling credential, and refuses — rather than silently substitutes a different conversation — for every way that verification can fail.
When this is not the right fit
This automation governs the public messaging API's own conversation-resolution logic — it does not change how conversations are found or created for integrators who never pass a conversationId, and it does not add a general cross-conversation search or listing capability; a caller can only resume a conversation whose exact id it already has.
Making an ignored parameter effective vs. making it effective AND safe
The narrow fix — simply reading the value that was already being written and using it — would have solved the resume problem while opening a new one: without an ownership check, any caller could attempt to resume any conversation id and either succeed illegitimately or receive a response that reveals whether that id is real. Building the ownership check and the identical-refusal design in the SAME change, rather than as a follow-up, is what keeps "make the parameter work" from becoming its own, separate security defect.
Related systems
Main system: Centriu Synapse.
What it does NOT do
- Does not let a caller list, search, or enumerate conversations — it can only resume a conversation whose exact id it already holds, and only after that id is verified as belonging to it.
- Does not reveal whether a given conversationId corresponds to a real conversation belonging to someone else — a foreign conversation and a nonexistent one receive the identical refusal.
- Does not change behavior for any request that omits the conversationId parameter — the historical find-or-create logic is preserved exactly, unchanged.
- Does not retroactively merge or reconcile conversations that were fragmented by the prior defect before this fix — an integrator with that concern should review its own stored conversation ids against what Synapse actually has for the affected window.
- Does not add conversation resumption for the voice channel or any channel beyond the public messaging API's own existing scope.
Security and governance
Centriu Synapse's messaging API resumes a caller-specified conversation only after verifying it belongs to that calling credential's own tenant and customer record, and collapses every kind of ownership mismatch into one identical, non-distinguishing refusal. Any personal data referenced remains subject to Brazil's LGPD (Law No. 13,709/2018). Full detail on access control lives at /governanca and /iso.
Pricing and contracting
Available by monthly subscription, with tiered plans. Values and terms come from the official pricing table at /precos (Centriu's central source — never restated here).
Frequently asked questions
Was the conversationId parameter really being ignored?
Confirmed directly by reading the code: the value was copied into a metadata field that a full-repository search shows is written in exactly one place and read in zero — nothing downstream ever consulted it.
What happens now if I ask to resume a conversation that belongs to someone else?
The exact same refusal you would get for a conversation id that does not exist at all — a single 404, by design, so the endpoint cannot be used to test which ids are real.
Does this change behavior for integrations that never use conversationId?
No — omitting the parameter preserves the historical find-or-create-the-open-conversation behavior exactly as it always worked.
Can I still see WHY a specific resume attempt failed?
The specific reason (not found, wrong tenant, wrong customer, or closed) is recorded in Centriu's own internal logs for operational debugging — it is deliberately never included in the API response itself.
Is this related to Centriu's AI agent and BYO key cross-tenant selection fix?
It comes from the same source commit but is a separate mechanism — that fix governs which agent or LLM key a message uses; this one governs which conversation a message attaches to.
What does Centriu Synapse cost?
It is sold by subscription with a published starting price — exact current values are on the central pricing page.
See how Centriu Synapse verifies conversation ownership before resuming it
Reach our commercial team directly, or leave your details below — we'll follow up with guidance for your case.