This page is for your backend (and, for chat, your app). It explains how to retry a request without doing the work twice.
Why you need it. Networks fail in an awkward way: sometimes the request reaches Livqeno and the work is done, but the response is lost on the way back. Your code sees a timeout and can't tell which case happened:
If you simply retry, you might create a second token, or post the same chat message twice. Idempotency (doing the same thing twice has the same effect as doing it once) solves this: you tag the operation with an id, and if Livqeno sees that id again, it returns the first result instead of doing the work again.
Livqeno has two ways to do this:
| You're retrying… | Use |
|---|---|
| A REST call (for example, minting a token with curl or your own HTTP client) | The Idempotency-Key header |
| A chat message (from any SDK, or over the WebSocket) | The clientMessageId field |
Over HTTP: the Idempotency-Key header
When you call the REST API directly from your backend, add an
Idempotency-Key header to the request:
curl -X POST "$LIVQENO_API_URL/v1/rooms/$ROOM_ID/rtc-tokens" \
-H "Authorization: Bearer $LIVQENO_API_KEY" \
-H "Idempotency-Key: 3f8a1c22-your-own-uuid" \
-H 'Content-Type: application/json' \
-d '{"participantIdentity":"user-42","permissions":{"join":true,"subscribe":true}}'If you retry with the same key, you get the original response back. No second token is made.
How to use it:
- Make a new UUID for each operation.
- Reuse that UUID only when retrying the same operation.
If two requests with the same key arrive at the same time, only one
runs. The other waits up to 2 seconds for the first one's response and
returns it. If the first one is still running after that, the other gets
409 RAVEN_IDEMPOTENCY_KEY_IN_USE with retryAfterSeconds. Retry with the
same key and you get the first response.
Reusing a key with a different body returns 409 RAVEN_CONFLICT.
The endpoint pages show which routes support it.
How long a replay lasts
| Endpoint | Replay window |
|---|---|
| Most idempotent endpoints | 24 hours |
| RTC token minting | 5 minutes |
Why so short for tokens? An RTC token can expire in as little as 30 seconds. Replaying a cached token an hour later would give you a dead credential. With 5 minutes, a retry either gets a still-useful token or a fresh one.
Chat: clientMessageId
Chat messages use a field, not a header. That's because messages also go over the WebSocket, which has no headers.
This is the case you'll hit most often: a user on a weak mobile connection
taps Send, the network drops, and your app retries. With the same
clientMessageId, the other people in the chat see the message once, not
twice. Make the id once when the user taps Send, and reuse it for every
retry of that message. In your app (or backend):
await chat.sendMessage({
text: 'Hello everyone',
clientMessageId: crypto.randomUUID(),
});A message counts as a duplicate when the conversation, the sender, and the
clientMessageId all match. The response tells you if it was a replay:
const result = await chat.sendMessage({ text: 'Hi', clientMessageId: id });
// result.deduplicated === true → this was a replay, not a new messageOver HTTP, a replayed message returns RAVEN_MESSAGE_ALREADY_EXISTS.
What isn't idempotent
Deletes and state changes (like ending a stream) don't use idempotency keys. They don't need them. They're already safe to retry:
- Ending a stream that already ended returns
RAVEN_STREAM_INVALID_STATE. - Deleting a room that's already
CLOSEDdoes nothing.
Next steps
- Conventions: the other rules every REST call follows (errors, request ids, pagination).
- Messages: sending, editing, and deleting chat messages.
- Errors: what each error code means.