Authentication and signing
Public API requests use AppKey / AppSecret with HMAC-SHA256 signatures. They do not depend on console login sessions or browser cookies.
Request headers
| Header | Description |
|---|---|
OC-App-Key | AppKey for the current workspace |
OC-Timestamp | Unix timestamp in milliseconds, within 5 minutes of platform time |
OC-Nonce | New random value per request, 16–64 characters: letters, digits, - or _ |
OC-Signature | v1= followed by the lowercase hexadecimal HMAC-SHA256 result |
X-Request-ID | Optional troubleshooting identifier, up to 64 characters |
Keep AppSecret on your server. Do not include it in webpages, mobile bundles or model context.
Signing content
Join these seven lines with newline characters, without a trailing newline:
OC-HMAC-SHA256
{UPPERCASE HTTP method}
{raw request path}
{raw query string, without the question mark}
{OC-Timestamp}
{OC-Nonce}
{SHA-256 of the request body, lowercase hex}Keep line four empty when there is no query string. Use the path and query exactly as sent, without additional sorting or decoding. Hash an empty byte sequence when there is no body.
Authentication self-check
GET /api/open/v1/auth/check and POST /api/open/v1/auth/check are implemented for signature validation. The platform supplies the API domain during integration.
This Node.js example calls only the self-check endpoint; it does not submit business messages:
import { createHash, createHmac, randomBytes } from 'node:crypto'
const { OMNICHAT_BASE_URL, OMNICHAT_APP_KEY, OMNICHAT_APP_SECRET } = process.env
if (!OMNICHAT_BASE_URL || !OMNICHAT_APP_KEY || !OMNICHAT_APP_SECRET) {
throw new Error('Configure the API domain, AppKey and AppSecret on your server')
}
const url = new URL('/api/open/v1/auth/check', OMNICHAT_BASE_URL)
if (url.protocol !== 'https:') throw new Error('Public APIs require HTTPS')
const body = JSON.stringify({ ping: 'hello' })
const timestamp = String(Date.now())
const nonce = randomBytes(16).toString('hex')
const digest = createHash('sha256').update(body, 'utf8').digest('hex')
const canonical = ['OC-HMAC-SHA256', 'POST', url.pathname,
url.search.slice(1), timestamp, nonce, digest].join('\n')
const signature = createHmac('sha256', OMNICHAT_APP_SECRET)
.update(canonical, 'utf8').digest('hex')
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'OC-App-Key': OMNICHAT_APP_KEY,
'OC-Timestamp': timestamp,
'OC-Nonce': nonce,
'OC-Signature': `v1=${signature}`,
},
body,
signal: AbortSignal.timeout(10000),
})
console.log(response.status, await response.text())Troubleshooting
- Signature mismatch: check that the sent body bytes match those used for the hash.
- Expired request: check your server clock and millisecond timestamp.
- Duplicate nonce: generate a fresh nonce and signature for every retry.
- Wrong workspace: verify that the credentials belong to your target workspace. Do not switch workspaces through the request body.
Callbacks use a separate signing key. See message callbacks.
Errors and rate limits
Business endpoints use POST JSON; the signature-check endpoint also supports GET. Request bodies are limited to 1 MB. The default rate limit is 600 requests per AppKey per calendar minute and may vary by deployment. A success response is {"code":"OK","message":"success","data":{}}. An error example:
{"code":"CONFLICT","message":"Check the current resource state","data":null}| HTTP | code | Handling |
|---|---|---|
| 400 | INVALID_ARGUMENT | Correct fields or configuration |
| 401 | AUTH_REQUIRED / INVALID_CREDENTIALS | Check credentials and signing |
| 401 | REQUEST_EXPIRED | Synchronize clocks and re-sign; OC-Server-Time gives platform milliseconds |
| 401 | REQUEST_REPLAYED | Generate a fresh nonce and signature; preserve the business eventId |
| 403 | CREDENTIAL_DISABLED / FORBIDDEN | Check workspace and credential status and IP allowlists |
| 404 | NOT_FOUND | Sync the user or check character identifiers |
| 409 | CONFLICT / VERSION_CONFLICT | Read current state before updating or retrying |
| 413 | PAYLOAD_TOO_LARGE | Reduce body size |
| 429 | RATE_LIMITED | Back off and retry with a fresh signature |
| 503 | SERVICE_UNAVAILABLE | Temporarily unavailable; retry later |
A network timeout does not prove the operation was not executed. Preserve eventId for chat retries. Query state after character creation/publication timeouts instead of retrying blindly. X-Request-ID is for tracing, not business deduplication.
Download the Node.js request-signing and callback-verification example; usage is shown in the quick start.