Official Kotlin SDK for Norbix. The SDK ships two top-level clients, one per plane:
NorbixApi(packageai.norbix.sdk.api) — project-scoped data. Default base URL:https://api.norbix.ai.NorbixHub(packageai.norbix.sdk.hub) — project / account configuration. Default base URL:https://hub.norbix.ai.
Modules are exposed as flat fields on each client, so call sites read like api.database.find(...) or hub.files.getFilesIntegrations(...).
| Client | Modules |
|---|---|
NorbixApi |
accessToken, ai, apikeys, auth, database, echo, files, membership, payments, publicProjects |
NorbixHub |
accessToken, account, ai, apikeys, auth, code, database, echo, email, environments, files, system, logs, membership, notifications, payments, regions, scheduler, webhooks |
Published to Maven Central as ai.norbix:norbix-kotlin. Gradle:
repositories {
mavenCentral()
}
dependencies {
implementation("ai.norbix:norbix-kotlin:1.10.0") // latest: https://central.sonatype.com/artifact/ai.norbix/norbix-kotlin
}Requires Java 17+; compiled for Kotlin 2.0+ consumers.
import ai.norbix.sdk.api.NorbixApi
val api = NorbixApi(apiKey = "sk_live_xxx", projectId = "proj_123")
val response = api.database.find(mapOf("collectionName" to "orders", "take" to 20))
println(response)Login flow (no API key, just user credentials):
import ai.norbix.sdk.api.NorbixApi
import ai.norbix.sdk.core.LoginCredentials
val api = NorbixApi(projectId = "proj_123")
val auth = api.login(LoginCredentials("alice@team.io", "secret"))
println(auth["bearerToken"])import ai.norbix.sdk.hub.NorbixHub
val hub = NorbixHub(
apiKey = "sk_live_xxx",
projectId = "proj_123",
)
val schemas = hub.database.getDatabaseSchemas()
val profile = hub.account.getAccountProfile()
println(profile)The push, email and SMS preview routes open with the signed link (hash)
alone — no API key or bearer token needed. When the client does have a
token it is still sent (scope OPTIONAL).
val hub = NorbixHub(projectId = "proj_123") // no apiKey, no bearerToken
val preview = hub.notifications.previewEmailNotification(mapOf("hash" to signedLink))
// also: previewPushNotification(...), previewSmsNotification(...)A signed-in member can pass projectId + notificationId instead of hash.
A bad or expired link comes back as a NorbixError with status 401.
An SMS campaign can go to the account's own people — the owner and team
members (deliveryType = "AccountUsers"). A member without a phone number is
skipped; each member saves their own number with updateMyAccountUserPhone.
The SMS template content has a body only (no subject — the sender is the
integration), and getSmsCampaigns takes an optional campaignId filter.
val hub = NorbixHub(bearerToken = token, projectId = "proj_123") // no accountId needed
hub.notifications.createSmsCampaign(mapOf(
"templateId" to "tmpl_1",
"integrationId" to "nbin_1",
"deliveryType" to "AccountUsers",
"accountUsers" to mapOf(
"recipientsSourceType" to "AccountUsers",
"recipients" to listOf("usr_owner", "usr_member"),
"campaignTime" to 1767225600, // unix seconds, UTC
),
))
// Your own team-member record (not the organisation profile) and your own phone.
val me = hub.account.getMyAccountUserProfile() // GET /{version}/account/me
hub.account.updateMyAccountUserPhone(mapOf("phone" to "+37060000000")) // PUT /{version}/account/me/phone; "" clears itThe one-message calls getSmsCampaignMessage, getEmailCampaignMessage and
getPushCampaignMessage (GET …/campaigns/{campaignId}/messages/{id}) are
gone: the gateway no longer serves that route. Use the list calls
(get*CampaignMessages) or the batch notification calls.
When several triggers fire for the same event they run as a queue. Two
optional fields on trigger (every save: schema, membership, files and
payments triggers) shape it: order — a whole number, 0 or more; lower runs
earlier, a trigger without an order runs after the numbered ones, equal places
run by name — and breakOnError — true stops the later triggers of the same
event when this trigger's action fails (default false). Both come back on the
get and list calls; a negative order fails with CM-ERRORS-TRIGGERS-008.
hub.membership.saveMembershipTrigger(mapOf(
"trigger" to mapOf(
"type" to "Membership",
"name" to "welcome first",
"when" to "OnRegistered",
"isEnabled" to true,
"order" to 1,
"breakOnError" to true,
"action" to mapOf("type" to "WebhookCall"),
),
))Norbix does not host code: a trigger's Code action, a scheduler task or a
manual call runs a function in your cloud (AWS Lambda, Azure Functions,
Google Cloud Functions) through a Code integration. hub.code lists the
functions an integration may call, their aliases and versions, invokes one by
hand, gives a sample envelope for a local run and the last runs of a trigger
or task — getCodeFunctions, getCodeFunctionQualifiers,
invokeCodeFunction, getCodeFunctionEventSample, getCodeFunctionRuns.
val functions = hub.code.getCodeFunctions(mapOf("integrationId" to "int_1", "prefix" to "orders-"))
val result = hub.code.invokeCodeFunction(
mapOf(
"functionRef" to mapOf("integrationId" to "int_1", "functionId" to "orders-on-update", "qualifier" to "prod"),
"payload" to mapOf("orderId" to "6ac54a59212c29912eaddd81", "dryRun" to true),
"invocationType" to "Sync",
),
)Details and the envelope the function receives: docs/hub/code.md.
Every constructor argument is optional and resolved in this order:
- Explicit constructor argument.
- Matching
NORBIX_*environment variable. - Built-in default.
| Constructor arg | Env variable | Default |
|---|---|---|
projectId |
NORBIX_PROJECT_ID |
— (required) |
apiKey |
NORBIX_API_KEY |
null |
bearerToken |
NORBIX_BEARER_TOKEN |
null |
accountId |
NORBIX_ACCOUNT_ID |
null (no call needs it: account calls take the account from the token; hub.account.verifyAccount takes accountId in its request) |
region |
NORBIX_REGION |
null (no region — see Regions) |
language |
NORBIX_LANGUAGE |
null (no Accept-Language — see Language) |
baseUrl (API) |
NORBIX_API_URL |
https://api.norbix.ai |
baseUrl (Hub) |
NORBIX_HUB_URL |
https://hub.norbix.ai |
version (API) |
NORBIX_API_VERSION |
v3 |
version (Hub) |
NORBIX_HUB_VERSION |
v3 |
This means a script that exports NORBIX_PROJECT_ID and NORBIX_API_KEY can call NorbixApi() with no arguments.
If you deploy Norbix to your own infrastructure, point the SDK at it through baseUrl or the matching env var:
val api = NorbixApi(
projectId = "proj_123",
apiKey = "sk_live_xxx",
baseUrl = "https://api.norbix.isidos.lt",
)
val hubLocal = NorbixHub(
projectId = "proj_123",
apiKey = "sk_live_xxx",
baseUrl = "http://localhost:5001",
)Or, without code changes:
export NORBIX_API_URL=https://api.norbix.isidos.lt
export NORBIX_HUB_URL=https://hub.norbix.isidos.ltNorbix can serve a project from multiple regions. The SDK targets a region by sending the nb-region header on every request. Unlike other options there is no default region: when no region is configured, no header is sent and the backend picks its own default.
The region resolves in the usual configuration order: explicit constructor argument → NORBIX_REGION environment variable → unset.
val hub = NorbixHub(projectId = "proj_123", apiKey = "sk_live_xxx", region = "nb-eu-germany")
val api = NorbixApi(projectId = "proj_123", apiKey = "sk_live_xxx", region = "nb-eu-germany")Or, without code changes:
export NORBIX_REGION=nb-eu-germanyBoth clients expose setRegion / getRegion:
hub.setRegion("nb-us-east") // subsequent requests send nb-region: nb-us-east
hub.getRegion() // "nb-us-east"
hub.setRegion(null) // unset: header no longer sent, default base URL restoredgetRegion() returns null when no region is set.
Methods on hub.regions (and Transport.send itself) accept an optional region argument that wins over the client's region for that single request. The override only changes the nb-region header — the base URL is not touched:
hub.regions.list(region = "nb-us-east")When the client points at the SDK default base URL, configuring a region (constructor, NORBIX_REGION, or setRegion) also composes a regional variant of that URL:
| Plane | Default | With region = "nb-eu-germany" |
|---|---|---|
| Hub | https://hub.norbix.ai |
https://nb-eu-germany.hub.norbix.ai |
| API | https://api.norbix.ai |
https://nb-eu-germany.api.norbix.ai |
setRegion(null) restores the plain default URL.
A custom base URL — passed to the constructor or set via NORBIX_HUB_URL / NORBIX_API_URL — is never rewritten, so self-hosted deployments are unaffected: the client keeps your URL and only sends the nb-region header.
hub.regions.list() (GET /{version}/account/regions, token only) lists the regions available to the account. Each item carries id (the region code), continent, and name:
{ "items": [ { "id": "nb-eu-germany", "continent": "Europe", "name": "Germany" } ] }hub.regions.updateProjectRegions(...) (PATCH /{version}/account/projects/{projectId}/settings/regions, token only) updates the project's regions. primaryRegion and additionalRegions are region code strings; pass null to leave one unchanged. Empty response on success:
hub.regions.updateProjectRegions(
projectId = "proj_123",
primaryRegion = "nb-eu-germany",
additionalRegions = listOf("nb-us-east"),
)The SDK is untyped on the wire, so hub.account.createProject takes a Map and passes the region keys straight through to the gateway:
hub.account.createProject(mapOf(
"name" to "My project",
"primaryRegion" to "nb-eu-germany",
"additionalRegions" to listOf("nb-us-east"),
))The gateway picks the answer's language from the standard Accept-Language header only (q-values are honoured; when nothing matches, the project's default language is used). The answer names the language it used in Content-Language. The old cultureCode request field and the CultureCode / norbix-language headers are gone; the SDK does not send them.
Set language on the client to send Accept-Language on every request. Like region there is no default: when unset, no header is sent. It resolves in the usual order: constructor argument → NORBIX_LANGUAGE → unset.
val api = NorbixApi(projectId = "proj_123", apiKey = "sk_live_xxx", language = "lt")
val hub = NorbixHub(projectId = "proj_123", apiKey = "sk_live_xxx", language = "lt-LT, en;q=0.8")
hub.setLanguage("en") // subsequent requests send Accept-Language: en
hub.getLanguage() // "en"
hub.setLanguage(null) // unset: header no longer sent
hub.regions.list(language = "de") // per-call override, same places as `region`
hub.transport.send(path = "/{version}/echo", method = "GET", language = "de")The language never changes the base URL. A client language replaces an Accept-Language entry in defaultHeaders.
try {
api.files.getFileInfo(mapOf("filesIntegrationId" to id, "path" to "a/b.txt"))
} catch (e: NorbixError) {
// httpStatus / errorCode are the names every Norbix SDK uses.
// status / code are the same values, kept for older code.
println("${e.httpStatus} ${e.errorCode}: ${e.message}")
e.errors.forEach { println("${it.errorCode} ${it.fieldName}: ${it.message}") }
println(e.body) // the answer exactly as it arrived
}message and errorCode are the gateway's own. The gateway puts them inside
responseStatus.errors[], so the SDK reads that list first, takes the first
entry for the message and the code, and keeps every entry in errors. Only
when the body has no responseStatus are the top-level message and
errorCode read. Request failed (HTTP N) with the code HTTP_<status> is
the last fallback, used when the body says nothing — a 500 page that is not
JSON, say.
The gateway answers a business refusal (an unknown id, a rule that says no)
with HTTP 200 and responseStatus.isSuccess = false. The SDK used to hand
that answer back as a normal value, so code carried on as if the call had
worked. It now throws a NorbixError with httpStatus 200 and the gateway's
message and error code.
If your code checked the responseStatus of the answer itself, move that check
into a try / catch. Endpoints that answer with raw bytes rather than a
document (sendBytes — file download, the public file link) are not JSON and
are unchanged.
Both clients implement AutoCloseable, so they work with use { }:
NorbixApi(projectId = "proj_123", apiKey = "sk_live_xxx").use { api ->
api.echo.echo()
}API/Hub modules, tests, and per-module docs are generated by the SDK maintenance workflow.
The major version is frozen at v1 until the public launch.
- A breaking change is released as a minor (for example v1.2.0 → v1.3.0), never as a new major.
- Write it as
feat(<scope>): <what>and add a lineBreaking: <what changed and what callers must do>in plain words, in the pull-request body and in the commit message. - Never mark it the conventional-commits way: no
!in the title (feat!:), no BREAKING CHANGE footer. ThePR titlecheck fails a pull request that does. - As a safety net, the release config (
.releaserc.json→releaseRules) maps breaking commits to a minor, so one that slips through still does not bump the major.
./gradlew test
./gradlew buildapi.ai is the end-user AI chat for a signed-in project user: availability,
sessions, entries, feedback, attachments, memory and startEndUserChatTurn,
which answers at once with a turnId. The answer streams over the gateway's
SSE endpoint on the user's own channel ai-chat:{projectId}:{authId}
(events ai.chat.turn.*, ai.chat.session.*); a subscription to another
user's channel is refused with 403 and
responseStatus.errorCode = "AiChatChannelRefused" before the stream starts —
do not retry it. This SDK has no SSE client. Path tokens are read from the
request map.
val turn = api.ai.startEndUserChatTurn(mapOf("sessionId" to sessionId, "message" to "What can you do?"))
val entries = api.ai.getEndUserChatEntries(mapOf("SessionId" to sessionId))Project owners configure the assistant on the Hub: hub.account
(getProjectAiSettings, updateProjectAiSettings, createProjectAiAssistant,
updateProjectAiAssistant, deleteProjectAiAssistant, getProjectAiUsage,
setAdminPortalEnabled) and hub.ai (getEmbeddingIntegrations,
saveEmbeddingIntegration, getEmbeddingIntegration,
deleteEmbeddingIntegration, testEmbeddingIntegration,
setLlmIntegrationAsDefault). Tables: docs/api/ai.md, docs/hub/ai.md,
docs/hub/account.md.
hub.account:updateProjectAdminUrl,updateProjectLegalDocuments,updateProjectExposeLegal,updateProjectExposeBrand,updateProjectExposeAuth,getAdminPortalStructure,assignAdminPortalServiceUser.api.publicProjects:getPublicProjectConfig,getPublicProjectLegal— no sign-in; sent with noAuthorizationheader.hub.account, AI service users:createAiServiceUser,listAiServiceUsers,rotateAiServiceUserKey,revokeAiServiceUserKey,deleteAiServiceUser.hub.account, developer MCP endpoint:sendMcpMessage,openMcpStream,endMcpSession. They return anMcpResponsethat carries the session id from theMcp-Session-Idanswer header.
hub.account.updateProjectLegalDocuments(mapOf("projectId" to projectId, "termsMarkdown" to "# Terms"))
hub.account.updateProjectExposeLegal(mapOf("projectId" to projectId, "exposed" to true))
hub.account.updateProjectExposeBrand(mapOf("projectId" to projectId, "exposed" to true))
hub.account.updateProjectExposeAuth(mapOf("projectId" to projectId, "exposed" to true))
val terms = api.publicProjects.getPublicProjectLegal(projectId = projectId, kind = "terms")Tables and the MCP example: docs/hub/account.md, docs/api/public_projects.md.