Ephemeral keys trigger Realtime sideband 404 errors

Our setup

The client does WebRTC. Our Node server sits in the middle of the SDP exchange and then opens a sideband WebSocket — the server-side control connection described in the Webhooks and server-side controls guide (wss://api.openai.com/v1/realtime?call_id=...) — to handle function calls on the server. The client never receives any OpenAI key; it only gets the answer SDP.

The two keys in the diagram are what this post is about:

Variant KEY A (creates the call in step 2) KEY B (sideband in step 5) How step 2 is sent
A — what we ran until now ephemeral key from POST /v1/realtime/client_secrets the same ephemeral key Content-Type: application/sdp, body = SDP
B — as shown in the guide ephemeral key standard API key same as A
C — unified interface standard API key standard API key multipart/form-data with fields sdp and session

What happened

Variant A had been running unchanged in production since 2025-10. On 2026-09-09, with no change on our side, step 5 started failing: the WebSocket handshake took about 5.5 seconds and returned

HTTP/1.1 404 Not Found
{
    "error": {
        "message": "No session found for the provided call_id",
        "type": "invalid_request_error",
        "code": "call_id_not_found",
        "param": ""
    }
}

Step 2 kept returning 201 with a valid answer SDP, and the client’s WebRTC leg connected normally (we received session.created on the data channel). Only the sideband could not find the call.

Test results on 2026-09-09, 03:20–03:50 UTC (while failing)

Variant Extra condition Sideband result
A (ephemeral → ephemeral) — 404 call_id_not_found (~5.5 s)
B (ephemeral → API key) — 404 call_id_not_found (~5.5 s)
B connect only after the client peer connection is connected; retry every ~6 s for 30 s 404 every time
A models gpt-realtime / gpt-realtime-mini; minimal session {type, model} 404
A / B extra OpenAI-Beta: realtime=v1 header, or &model= in the URL 404
C (API key → API key) — connected; events and function calls work

Same tests on 2026-09-09, 05:36 UTC (about two hours later)

Variant Sideband result (4 runs each)
A (ephemeral → ephemeral) connected 4/4 (~0.4 s)
B (ephemeral → API key) 404 call_id_not_found 4/4 (~5.5 s)
C (API key → API key) connected

So in the steady state the sideband connects only with the same key that created the call (A and C work, B does not), and for about two hours on Sep 9 even variant A failed. We switched production to variant C, which worked throughout.

Environment

  • Node.js 22, ws 8.18.3, built-in fetch / FormData
  • Model gpt-realtime-1.5
  • Requests from Japan (AWS ap-northeast-1 and a local machine); call IDs are prefixed rtc_u7_

Minimal reproduction (Node 22)

Runs the three variants in order. The SDP is a syntactically valid but non-connecting offer; the sideband result does not depend on media being connected (see the 30-second retry row above).

// node --input-type=module - < repro.mjs   (OPENAI_API_KEY must be set)
import WebSocket from 'ws'

const apiKey = process.env.OPENAI_API_KEY
const session = { type: 'realtime', model: 'gpt-realtime-1.5' }
const fp = Array.from({ length: 32 }, (_, i) => ((i * 7) % 256).toString(16).padStart(2, '0').toUpperCase()).join(':')
const sdp = [
  'v=0', 'o=- 4611731400430051336 2 IN IP4 127.0.0.1', 's=-', 't=0 0',
  'a=group:BUNDLE 0', 'a=msid-semantic: WMS',
  'm=audio 9 UDP/TLS/RTP/SAVPF 111', 'c=IN IP4 0.0.0.0', 'a=rtcp:9 IN IP4 0.0.0.0',
  'a=ice-ufrag:abcd', 'a=ice-pwd:abcdefghijklmnopqrstuvwx', 'a=ice-options:trickle',
  `a=fingerprint:sha-256 ${fp}`, 'a=setup:actpass', 'a=mid:0', 'a=recvonly', 'a=rtcp-mux',
  'a=rtpmap:111 opus/48000/2', 'a=fmtp:111 minptime=10;useinbandfec=1', '',
].join('\r\n')

const callIdFrom = r => r.headers.get('Location')?.split('/').pop()

function sideband(callId, key) {
  return new Promise(resolve => {
    const ws = new WebSocket(`wss://api.openai.com/v1/realtime?call_id=${callId}`, {
      headers: { Authorization: `Bearer ${key}` },
    })
    ws.once('open', () => { resolve('OPEN'); ws.close() })
    ws.once('unexpected-response', (_req, res) => {
      let body = ''
      res.on('data', d => (body += d))
      res.on('end', () => resolve(`HTTP ${res.statusCode} ${body}`))
    })
    ws.once('error', e => resolve(`ERROR ${e.message}`))
  })
}

async function createWithEphemeralKey() {
  const secret = await fetch('https://api.openai.com/v1/realtime/client_secrets', {
    method: 'POST',
    headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ session }),
  }).then(r => r.json())
  const res = await fetch('https://api.openai.com/v1/realtime/calls', {
    method: 'POST',
    headers: { Authorization: `Bearer ${secret.value}`, 'Content-Type': 'application/sdp' },
    body: sdp,
  })
  return { callId: callIdFrom(res), ephemeralKey: secret.value, status: res.status }
}

// A) ephemeral-key call, sideband with the same ephemeral key
const a = await createWithEphemeralKey()
console.log('A) ephemeral-key call', a.status, a.callId, '-> sideband(ephemeral):', await sideband(a.callId, a.ephemeralKey))

// B) ephemeral-key call, sideband with the API key (new call: closing a sideband ends the call)
const b = await createWithEphemeralKey()
console.log('B) ephemeral-key call', b.status, b.callId, '-> sideband(API key):  ', await sideband(b.callId, apiKey))

// C) unified interface: API key creates the call, API key joins the sideband
const form = new FormData()
form.append('sdp', sdp)
form.append('session', JSON.stringify(session))
const c = await fetch('https://api.openai.com/v1/realtime/calls', {
  method: 'POST',
  headers: { Authorization: `Bearer ${apiKey}` },
  body: form,
})
console.log('C) API-key call      ', c.status, callIdFrom(c), '-> sideband(API key):  ', await sideband(callIdFrom(c), apiKey))
process.exit(0) // keep-alive sockets otherwise hold the process open for a while

Output while failing (Sep 9, ~03:30 UTC): A = 404 call_id_not_found, B = 404, C = OPEN.
Output two hours later (Sep 9, 05:36 UTC): A = OPEN, B = 404 call_id_not_found, C = OPEN.

Questions

  1. For this setup — our server relays the SDP and the client never holds a key — is variant C (API key creates the call, API key joins the sideband) the recommended pattern? We see no reason to keep the ephemeral-key step, but want to confirm before settling on it.
  2. Was the sideband 404 for ephemeral-key calls on Sep 9 a temporary instability on the platform side? Nothing appeared on the status page or in the changelog. A SIP report with the same ~5 s delay and the same error body is here; support said it “does not appear expected”.
  3. Variant B is what the guide shows (Bearer ${EPHEMERAL_KEY} for /calls, Bearer OPENAI_API_KEY for the sideband), but it returns 404 for us both while failing and after. If “the sideband must use the same key that created the call” is the intended rule, could the guide say so?

Update: it is happening again today.

2026-09-10, 00:43 UTC — same tests as in the original post, same account and model:

Variant Result (3 runs)
A (ephemeral key creates the call → ephemeral key on the sideband) 404 call_id_not_found 3/3, ~5.5 s each
B (ephemeral key → API key on the sideband) 404 call_id_not_found 2/2
C (API key creates the call → API key on the sideband, unified interface) connected 3/3, ~0.4 s

The 404 responses carry no x-request-id; cf-ray values were a38a65d9bd43a59e-NRT, a38a660129b9e37f-NRT, a38a6627d911e357-NRT (variant A) and a38a664e58eb7e2f-NRT, a38a6675ef623c02-NRT (variant B).

Two more data points from the same run:

  • An ephemeral key still opens a plain Realtime WebSocket session (wss://api.openai.com/v1/realtime?model=...) without any problem, so the ephemeral key itself is fine. Only the sideband lookup for a call created with that key fails.
  • Retrying the sideband on the same ephemeral-key call a second time (after the first 5.5 s 404) returns the same 404.
  • Waiting for the client does not help either: with the browser’s peer connection connected and session.created received on the data channel (00:49 UTC), the sideband for that ephemeral-key call still returned 404 at 5 s, 12 s, 18 s and 25 s.

Our production, which now uses variant C, is unaffected; the previous version of our code (variant A) fails right now.

I noticed one other change in C: the request switches from application/sdp to multipart with a session field. Could that be part of the difference? The repeat runs make C look consistent, but I’d be hesitant to attribute it to the key alone while both variables change.

@joinwell52 Thanks, that is a fair point — variant C did change two things at once. I separated them: key (ephemeral vs standard API key) × request format (application/sdp vs multipart with a session field), with the sideband always using the key that created the call.

2026-09-10, 02:25–02:31 UTC, three consecutive rounds, identical every time:

Call created with Request format Sideband result
ephemeral key application/sdp 404 call_id_not_found
ephemeral key multipart (sdp + session) 404 call_id_not_found
standard API key application/sdp (with ?model= in the query) connected
standard API key multipart (sdp + session) connected

All four calls were created successfully (201 with a valid answer SDP); only the sideband differs. So the request format is not the variable — the key that creates the call is. application/sdp with a standard API key works fine, so nothing about the old-style request is broken. This also matches the reverse case from the first post: a call created with the standard API key returns 404 on the sideband if the sideband presents an ephemeral key.

For the record, the ephemeral-key path stayed broken for us far longer today than yesterday: we probed every 5 minutes from 00:45 to 04:09 UTC and every single attempt failed, while the API-key path succeeded every time.

On our side we are settling on the API-key (unified interface) approach:

  • The official realtime-server-controlled example added on 2026-08-27 uses a standard API key for both the call creation and the sideband, for exactly this architecture.
  • Looking back at our own code, we originally planned the browser-direct flow where the browser receives an ephemeral token, and we kept minting one after we moved the SDP exchange to the server. That step stopped being necessary at that point and we simply never revisited it, so we are dropping it.

Yeah, I think the key detail here is that you separated the two variables. That makes the 404 behavior much easier to understand. If both request formats work with the standard API key but the sideband fails with the ephemeral key, then the key used to create the call seems to be the real difference.

The part about switching to the server side API key flow also makes sense for this setup.

Thanks for running the four combinations, Akira—that answers my concern about the request format. In those runs, the split follows the credential used to create the call, across both formats. The failing ephemeral case is still worth keeping separate from the working server-side path; switching production avoids it, but doesn’t explain why it changed.

Hi @akira_numakura @joinwell52

We started seeing this issue around the same time , starting 10th Sept. The sideband wss connection gets rejected by openai using the ephemeral key, when it was working since the last 4 months. testing with the key chat created the call in the first place, creates a successful connection.
Is there any official acknowledgement, or confirmation on this issue/sudden change from OpenAI team that you know of?

I haven’t found an official acknowledgment of this specific sideband issue yet, tanuj—I checked this thread, the status history, and the API changelog. Akira’s tests are the evidence I’ve been following; we haven’t reproduced it ourselves. So I can’t confirm whether the change was intentional or a regression.

I contacted support because the same issue occurred in our commercial service for customers.
As a result, they acknowledged it as a bug and fixed it on September 15, 2026, at 06:51 JST (September 14, 2026, at 21:51 UTC).
Call creation using ephemeral keys and Sideband connections is now working again.