Skip to content

Soprano: switch to the omnimsg endpoint contract - #3

Open
Hou (SciencePotato) wants to merge 4 commits into
add-cyot-samplefrom
soprano-omnimsg
Open

Soprano: switch to the omnimsg endpoint contract#3
Hou (SciencePotato) wants to merge 4 commits into
add-cyot-samplefrom
soprano-omnimsg

Conversation

@SciencePotato

@SciencePotato Hou (SciencePotato) commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Switches the Soprano adapter to the omnimsg contract from Soprano's CYOT integration guide, in all three language implementations.

What changed

  • One endpoint for every channel: POST {EPP_PROVIDER_ENDPOINT}/messages/omnimsg replaces /messages/sms and /messages/voice.
  • Body follows the documented contract: text, destination (E.164 with the leading + stripped), messageTypes: ["sms"|"voice"], correlationId, shutterMode.
  • Removed: messageType, clientReference, the voice.text2voice block, and the endpoints/source sender fields — omnimsg renders the voice TTS from the same text and takes the sender from the account provisioning. SOPRANO_SOURCE_TYPE, SOPRANO_VOICE_LANGUAGE and SOPRANO_VOICE_GENDER are no longer read.
  • Added optional SOPRANO_SHUTTER_MODE app setting — sends shutterMode: true so Soprano processes the request but delivers nothing, for connectivity and credential testing. This is distinct from the pipeline's own evaluation-mode shutter, which still returns before any HTTP call is made.
  • FILTERED is mapped to Fail — see below.
  • parseResponse is unchanged; it already handled {id, destination, status}.

Verified end to end against Soprano QA4

Both channels confirmed working on the live QA4 environment, against a destination Soprano confirmed is routable:

Channel Provider response Pipeline result
sms 201{"id":400004307033,"destination":"61434024858","status":"ENROUTE"} 200 / Continue / accepted
voice 201{"id":400004307034,…,"status":"ENROUTE"} 200 / Continue / accepted

Response body from the pipeline:

{
  "status": "accepted",
  "outcome": "Continue",
  "provider": "soprano",
  "channel": "sms",
  "providerMessageId": "400004307033",
  "providerStatus": "ENROUTE",
  "requestId": "req-..."
}

Why FILTERED is mapped to Fail

Destinations outside the account's routing come back as 201 with status: "FILTERED" rather than an error — accepted, then silently dropped, with no reason field. Observed on US destinations and on one AU number, identically on the previous /messages/sms path and with or without a sender field, so it is a routing decision rather than a payload problem. Mapping it explicitly to Fail documents the behaviour instead of relying on the fail-closed default.

For contrast, a genuinely malformed request is reported properly: an invalid destination returns 400 with errorCode 400102 and Invalid destination address, which flows through to providerStatusDescription.

Open question for Soprano

Does FILTERED ever indicate a policy block (blocklisted MSISDN) in production? If so it may belong on Block/403 rather than Fail/502.

Note: Entra ID v2.0 bearer auth is not wired end-to-end — there is no outbound token acquisition, so oauth2 mode fails closed and QA4 runs on the X-MEMS-* headers.

Tests

javascript 37 passed, python 24 passed, dotnet 23 passed.

Hou Chi Chan added 4 commits August 24, 2026 09:40
One endpoint for every channel: POST {base}/messages/omnimsg with text/destination/messageTypes/correlationId/shutterMode. Soprano renders voice TTS from the same text, so the text2voice block and the provisioned-source fields go away; destination is E.164 without the leading +. SOPRANO_SHUTTER_MODE opts into Soprano-side shutter for connectivity tests.
QA4 answers 201 with status FILTERED when an account/destination filter stops delivery, on both the omnimsg and the legacy per-channel path. Pin it instead of leaning on the fail-closed default.
Adds an omnimsg payload test per language (endpoint, messageTypes, destination without the leading +, passcode in text) - the rewrite previously rode on suites that never asserted the new shape. Makes the .NET destination null-safe like the other two, and strips every leading + in JavaScript so all three agree. Documents SOPRANO_SHUTTER_MODE as diagnostics-only, since a 2xx with a matching nonce stops SAS falling back, and records that Soprano voice ignores locale.
Two deviations from the CYOT endpoint contract. The type field was parsed and logged but never validated, so a future contract version would have been interpreted as v1; it is now pinned per language. ttlSeconds <= 0 was logged and delivered anyway, which spends a message on a code that can no longer authenticate; all three now return 400 and dispatch nothing. CONTRACT.md said the old behaviour was intended, so it is corrected alongside.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant