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

On this page

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

  1. Pick the fields that have a value. Empty and missing fields are left out, and so is the token.
  2. Sort the fields by name (lowercase ASCII, byte order).
  3. Make one line name=value per field, where the value is its UTF-8 bytes written as lowercase hexadecimal.
  4. Join uchat-v2 and the lines from step 3, separated by a single newline (\n) each.
  5. Compute HMAC-SHA256 of the step 4 string with the chat room token as the key. The hash is the 64 lowercase hex characters prefixed with v2..
  6. In the same order, join name + space + encodeURIComponent(value) with |, and append hash value at 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, use rawurlencode. If you encode extra characters such as ~, *, and ! (as rawurlencode does), they decode to the same value, so the hash is the same.
  • The chat window decodes each value with decodeURIComponent and sends it to the server. The server recomputes the hash in the same order and compares. The hex in hash is case-insensitive.
  • HMAC-SHA256 and hex conversion are in the standard library of most languages (PHP 5.6+ hash_hmac·bin2hex, Node.js crypto, Python hmac·hashlib, Java javax.crypto.Mac, C# HMACSHA256, Go crypto/hmac, Ruby OpenSSL::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 offset as the reference (first join).
  • If a reference exists and offset is 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 hash for only 300 seconds after it first receives it. After that, you get error 205.
  • The reference moves to the new offset only when a join passes with a hash the server has not seen before. Reusing the same user_data does not move it.
  • Turning on Fixed standard time sets the reference to 0. A time more 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 time that 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.

Business name UCHAT · CEO 김경욱 · Business registration no. 141-05-32832
Address 경기도 파주시 와석순환로 61 · Mail-order business registration no. 2013-경기파주-4485호
Phone 010-8677-8633 · Email support@uchat.io

ⓒ 2013–2026 UCHAT. All rights reserved.