Member integration signature spec
The fields of the member integration user_data string, how to build the signature (hash), the time sync checks, and fixed values for testing.
Last updated October 19, 2026
In member integration, user_data is a string your server signs from member info using the chat room token. This page is the authoritative reference for the fields, the signing order, and the rules the server checks. For the setup flow and examples in each language, see Member integration.
Fields
Only the fields below that have a value are signed. They are signed in field name order, so the order you add them in does not matter. The signing code signs every field you add, so adding a field missing from this table causes error 202. If fields are added later, the signature of a site that does not send the new fields stays the same.
| Field | Format | Description |
|---|---|---|
auth |
0–3 or guest, member, subadmin, admin |
Permission. 0 guest, 1 member, 2 moderator, 3 admin. If empty: member when id is set, otherwise guest |
icons |
List of URLs | Icons next to the nickname. Separate several with commas; those before @ go before the nickname, those after go after it. Example: /img/a.png,/img/b.png@/img/c.png |
id |
String | Member ID. The basis for bans, mutes, and granted permissions |
level |
Number | Level. Used for level comparisons in chat room feature conditions |
nick |
String | Nickname. The server cuts it to 15 characters, then removes whitespace |
nickcon |
URL | Image shown instead of the nickname |
other |
String | Extra display. Separate words with a single space. Usually left empty |
room |
Chat room ID | Required. Must match the <u-chat room> value |
time |
Unix time (seconds) | Required. The current time when you create the signature. Not checked when time sync is off, but always include it |
The signing key is the chat room token. Find it in the admin page under Chat room info → Token. Do not put it in the resulting string. It is a different value from the API key for the server REST API. If the token leaks, anyone can join as an admin, so keep it only in your server code.
Signing order
- Pick the fields that have a value. Empty and missing fields are left out, and so is the token.
- Sort the fields by name (lowercase ASCII, byte order).
- Make one line
name=valueper field, where the value is its UTF-8 bytes written as lowercase hexadecimal. - Join
uchat-v2and the lines from step 3, separated by a single newline (\n) each. - Compute HMAC-SHA256 of the step 4 string with the chat room token as the key. The
hashis the 64 lowercase hex characters prefixed withv2.. - In the same order, join
name + space + encodeURIComponent(value)with|, and appendhash valueat the end.
Result shape:
icons ...|id ...|level ...|nick ...|room my-room|time 1700000000|hash v2.0123...(64 chars)
- Encode values like JavaScript
encodeURIComponent. A space becomes%20. In PHP, userawurlencode. If you encode extra characters such as~,*, and!(asrawurlencodedoes), they decode to the same value, so the hash is the same. - The chat window decodes each value with
decodeURIComponentand sends it to the server. The server recomputes the hash in the same order and compares. The hex inhashis case-insensitive. - HMAC-SHA256 and hex conversion are in the standard library of most languages (PHP 5.6+
hash_hmac·bin2hex, Node.jscrypto, Pythonhmac·hashlib, Javajavax.crypto.Mac, C#HMACSHA256, Gocrypto/hmac, RubyOpenSSL::HMAC).
Reference implementation (Node.js):
const crypto = require('node:crypto');
function signUserData(user, token) {
const fields = Object.entries({ ...user, time: Math.floor(Date.now() / 1000) })
.filter(([, v]) => v !== undefined && v !== null && v !== '')
.map(([k, v]) => [k, String(v)])
.sort(([a], [b]) => (a < b ? -1 : 1));
const message = ['uchat-v2', ...fields.map(([k, v]) => k + '=' + Buffer.from(v, 'utf8').toString('hex'))].join('\n');
const hash = crypto.createHmac('sha256', token).update(message, 'utf8').digest('hex');
return fields
.map(([k, v]) => k + ' ' + encodeURIComponent(v))
.concat('hash v2.' + hash)
.join('|');
}
// signUserData({ room: 'my-room', nick: '홍길동', id: 'user1', level: 5 }, process.env.UCHAT_TOKEN)
Fixed values for testing
Use the input below to check your own code. You should get the same result.
| Input | Value |
|---|---|
| Token | test-token-1234 |
time |
1700000000 |
room |
my-room |
nick |
홍 길동~*! |
id |
user@example.com |
level |
5 |
auth |
(empty) |
icons |
/img/a.png,/img/b.png@/img/c.png |
The string fed to HMAC (step 4; the empty auth has no line):
uchat-v2
icons=2f696d672f612e706e672c2f696d672f622e706e67402f696d672f632e706e67
id=75736572406578616d706c652e636f6d
level=35
nick=ed998d20eab8b8eb8f997e2a21
room=6d792d726f6f6d
time=31373030303030303030
Resulting user_data:
icons %2Fimg%2Fa.png%2C%2Fimg%2Fb.png%40%2Fimg%2Fc.png|id user%40example.com|level 5|nick %ED%99%8D%20%EA%B8%B8%EB%8F%99~*!|room my-room|time 1700000000|hash v2.80d95cf251e7aeef90b37547dbe95ef5336f6c7f8cb235a6cf6cee91031e2ae6
auth is empty, so it is left out of both user_data and the signature. If you build it with PHP rawurlencode, ~, *, and ! become %7E, %2A, and %21. The text differs, but the hash is the same and it works as is.
Server checks
In a chat room with member integration on, the chat server checks the following, in order, when someone joins.
| Check | Error on failure |
|---|---|
Is there a hash? |
203 |
| Is the hash correct (recomputed in the order above)? | 202 |
If time sync is on: is time not older than the reference, and has less than 300 seconds passed since the same signature was first used? |
205 |
The signature has no one-time value (nonce). With time sync off, anyone can copy a user_data and reuse it at any time, so keep time sync on.
Time sync
time is checked only when you turn on Time sync on the Member integration screen of the admin page. When it is off, time is only part of the signature and its value is not checked.
- The server calculates
offset = time - current time on the uchat server. - If no reference is stored yet, it stores this
offsetas the reference (first join). - If a reference exists and
offsetis more than 300 seconds smaller than the reference (stale join data), it rejects with error 205. Future times are not blocked. - The chat server accepts the same
hashfor only 300 seconds after it first receives it. After that, you get error 205. - The reference moves to the new
offsetonly when a join passes with ahashthe server has not seen before. Reusing the sameuser_datadoes not move it. - Turning on Fixed standard time sets the reference to 0. A
timemore than 300 seconds behind the uchat server clock is rejected. Use this when your server clock is accurate. - If you move your server or set its clock back by more than 300 seconds, reset the Time reference in the admin page.
- In a chat room with time sync on, a
timethat is not a number gives error 101.
If you cache user_data in the page, error 205 can start after 5 minutes. Don't cache pages with member integration, or fetch it fresh each time with user_data_url (u-chat attributes). For fixes by error, see Member integration.
Still not sure how it works? Ask your question on the contact board.