Official Swift SDK for Norbix. The SDK ships as two separate libraries that mirror the two Norbix planes:
NorbixApi— project-scoped data operations (defaulthttps://api.norbix.ai)NorbixHub— project / account configuration operations (defaulthttps://hub.norbix.ai)
A small shared NorbixCore module holds the transport, config, and error types
both clients use.
| Client | Modules |
|---|---|
NorbixApiClient |
accessToken, ai, apiKeys, auth, database, echo, files, membership, payments, publicProjects |
NorbixHubClient |
accessToken, account, ai, apiKeys, auth, code, database, echo, email, environments, files, logs, membership, notifications, payments, platform, regions, scheduler, webhooks |
Built for Apple platforms (iOS, macOS, tvOS, watchOS, visionOS) with standard Swift tooling: Swift Package Manager, XCTest, GitHub Actions CI, and semantic-release based tagging.
In Xcode:
- File -> Add Packages...
- Use URL:
https://github.com/norbix-code/sdk-swift - Pick a version rule (recommended: Up to Next Major)
- Add
NorbixApiand/orNorbixHubto your target — only the planes you need.
Or in Package.swift:
.package(url: "https://github.com/norbix-code/sdk-swift.git", from: "1.0.0").target(
name: "MyApp",
dependencies: [
.product(name: "NorbixApi", package: "sdk-swift"),
// .product(name: "NorbixHub", package: "sdk-swift"),
]
)import NorbixApi
let client = try NorbixApiClient(projectId: "proj_123", apiKey: "sk_live_xxx")
let _ = try await client.database.getDatabaseSchemas()
let me = try await client.membership.getCurrentUser()Login flow:
import NorbixApi
import NorbixCore
let client = try NorbixApiClient(projectId: "proj_123")
let _ = try await client.login(.init(userName: "alice@team.io", password: "secret"))
let me = try await client.membership.getCurrentUser()import NorbixHub
let hub = try NorbixHubClient(
projectId: "proj_123",
apiKey: "sk_live_xxx"
)
let account = try await hub.account.getAccountProfile() // no accountId needed
let integrations = try await hub.files.getFilesIntegrations()
// Sign-up, invitation, regions and verify need no token and no accountId:
let anon = try NorbixHubClient(projectId: "proj_123") // no apiKey, no bearerToken
let regions = try await anon.account.getAccountRegions()Both clients accept a baseUrl parameter. Use it for self-hosted Norbix or
local development:
// Custom company domain
let client = try NorbixApiClient(
projectId: "proj_123",
apiKey: "sk_live_xxx",
baseUrl: "https://api.norbix.isidos.lt"
)
// Local development
let hub = try NorbixHubClient(
projectId: "proj_123",
apiKey: "sk_live_xxx",
baseUrl: "http://localhost:5000"
)If you do not pass baseUrl, the SDK uses Norbix' production endpoints:
https://api.norbix.ai and https://hub.norbix.ai.
Norbix projects can live in more than one region. The SDK targets a region
by sending its code (e.g. nb-eu-germany) in the nb-region header.
There is no default region: when no region is configured, no header is
sent and the backend picks its own default.
Pass region when constructing either client:
let client = try NorbixApiClient(
projectId: "proj_123",
apiKey: "sk_live_xxx",
region: "nb-eu-germany"
)
let hub = try NorbixHubClient(
projectId: "proj_123",
apiKey: "sk_live_xxx",
region: "nb-eu-germany"
)Or set the NORBIX_REGION environment variable — the convenience
initializers and NorbixConfig.fromEnvironment fall back to it when no
region argument is given:
NORBIX_REGION=nb-eu-germany
NORBIX_LANGUAGE="lt-LT, en;q=0.8" # optional: sent as Accept-LanguageBoth clients expose setRegion(_:) and a region getter:
hub.setRegion("nb-eu-germany")
print(hub.region ?? "unset") // "nb-eu-germany"
hub.setRegion(nil) // unset: no nb-region header is sent again
print(hub.region ?? "unset") // "unset"Passing nil (or an empty string) unsets the region. Unlike setEnv
there is no value that means "the default region" — unset simply omits the
header.
The hub.regions methods accept a region: argument that overrides the
client's region for that single request. The override affects the
nb-region header only — it never changes the request URL:
let regions = try await hub.regions.getAccountRegions([:], region: "nb-us-east")When a client uses an SDK-default base URL, configuring a region (at init
or via setRegion) also composes the regional endpoint by prefixing the
region code as a subdomain:
https://api.norbix.ai→https://nb-eu-germany.api.norbix.aihttps://hub.norbix.ai→https://nb-eu-germany.hub.norbix.ai
setRegion(nil) restores the default URL. A custom baseUrl (self-hosted
/ on-prem / local development) is never rewritten: the nb-region
header is still sent, but the URL stays exactly as you configured it.
hub.regions wraps the regions endpoints. getAccountRegions needs no token
and no accountId (the gateway route is anonymous); updateProjectRegions
works with a token only (the client needs no accountId):
// GET /v2/account/regions — regions available to the account.
// Response carries "items"; each item has "id" (the region code,
// e.g. "nb-eu-germany"), "continent", and "name".
let available = try await hub.regions.getAccountRegions([:])
// PATCH /v2/account/projects/{projectId}/settings/regions
// Pass nil to leave a field unchanged. Empty response on success.
_ = try await hub.regions.updateProjectRegions(
projectId: "proj_123",
primaryRegion: "nb-eu-germany",
additionalRegions: ["nb-us-east"]
)The SDK is untyped on the wire, so region fields on other endpoints pass straight through — for example, creating a project with a primary region:
_ = try await hub.account.createProject([
"primaryRegion": "nb-eu-germany"
// ...other project fields
])The gateway picks the response language from the standard Accept-Language
header only (q-values are honoured; the project's default language applies
when nothing matches) and names the language it used in the
Content-Language response header. There is no default language: when
none is configured, no header is sent.
Pass language when constructing either client — a single tag or a full
q-value list, sent verbatim:
let client = try NorbixApiClient(
projectId: "proj_123",
apiKey: "sk_live_xxx",
language: "lt"
)
let hub = try NorbixHubClient(
projectId: "proj_123",
apiKey: "sk_live_xxx",
language: "lt-LT, en;q=0.8"
)Or set the NORBIX_LANGUAGE environment variable — the convenience
initializers and NorbixConfig.fromEnvironment fall back to it when no
language argument is given:
NORBIX_LANGUAGE="lt-LT, en;q=0.8"Switch at runtime with setLanguage(_:) and read it back with language;
nil (or an empty string) unsets it, so no header is sent again:
hub.setLanguage("en")
print(hub.language ?? "unset") // "en"
hub.setLanguage(nil) // unset: no Accept-Language headerWherever a per-call region: exists (the hub.regions methods and the
Transport.send / downloadData calls) a per-call language: overrides the
client's language for that single request:
let regions = try await hub.regions.getAccountRegions([:], language: "en")The old cultureCode request field and the CultureCode /
norbix-language headers are no longer read by the gateway; use language.
import NorbixApi
import NorbixCore
let client = try NorbixApiClient(projectId: "proj_123", apiKey: "sk_live_xxx")
do {
let response = try await client.database.find([
"collectionName": "orders",
"take": 20,
"skip": 0,
])
if let payload = response as? [String: Any] {
print("orders response:", payload)
}
} catch let error as NorbixError {
print(error.code, error.status, error.message)
}import NorbixApi
let client = try NorbixApiClient(projectId: "proj_123")
let auth = try await client.login(.init(userName: "alice@team.io", password: "secret"))
print("logged in:", auth["bearerToken"] ?? "")
let me = try await client.membership.getCurrentUser()
print(me)Account calls take the account from the signed-in token, so the client needs
no accountId.
Four account calls need no token at all (scope .unauthenticated, no
Authorization header): createAccount (sign-up),
createTeamMemberFromInvitation, getAccountRegions (also
hub.regions.getAccountRegions) and verifyAccount. verifyAccount takes the
account id once, in the request: pass accountId and token from the
verification email link; they go in the query.
import NorbixHub
let hub = try NorbixHubClient(
projectId: "proj_123",
apiKey: "sk_live_xxx"
)
let account = try await hub.account.getAccountProfile()
print(account)
// No token, no accountId:
let anon = try NorbixHubClient(projectId: "proj_123")
_ = try await anon.account.verifyAccount(["accountId": "acc_456", "token": codeFromEmail])A record read with expandReferences: true answers every reference (a user,
a role, a taxonomy term, a record of another collection, a file) as
{ id, display }. Declare the field as ExpandedReference from NorbixCore:
import NorbixApi
import NorbixCore
struct Post: Codable, Sendable {
let title: String
let author: ExpandedReference
let tags: [ExpandedReference]
}
let page: Page<Post> = try await client.database.find(
collection: "posts", expandReferences: true, as: Post.self
)
print(page.items[0].author.displayText() ?? "?") // Jane DoeMore in API · Database → Linked records and Files by id.
- API key mode:
apiKeyorNORBIX_API_KEY - Bearer mode:
bearerToken,NORBIX_BEARER_TOKEN, orclient.login(...) - If both set, bearer token wins
- If none set, the SDK throws
NORBIX_NOT_AUTHENTICATEDon the first authenticated call - Anonymous calls send no
Authorizationheader, even when the client has a token:hub.account.createAccount,createTeamMemberFromInvitation,getAccountRegions,verifyAccount, andhub.regions.getAccountRegions
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).
let hub = try NorbixHubClient(projectId: "proj_123") // no apiKey, no bearerToken
let preview = try await hub.notifications.previewEmailNotification(["hash": signedLink])
// also: previewPushNotification(...), previewSmsNotification(...)A signed-in member can pass projectId + notificationId instead of hash.
A bad or expired link throws 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.
let hub = try NorbixHubClient(projectId: "proj_123", bearerToken: token) // no accountId needed
_ = try await hub.notifications.createSmsCampaign([
"templateId": "tmpl_1",
"integrationId": "nbin_1",
"deliveryType": "AccountUsers",
"accountUsers": [
"recipientsSourceType": "AccountUsers",
"recipients": ["usr_owner", "usr_member"],
"campaignTime": 1767225600, // unix seconds, UTC
],
])
// Your own team-member record (not the organisation profile) and your own phone.
let me = try await hub.account.getMyAccountUserProfile() // GET /{version}/account/me
_ = try await hub.account.updateMyAccountUserPhone(["phone": "+37060000000"]) // PUT /{version}/account/me/phone; "" clears itThe one-message calls getSmsCampaignMessage and getEmailCampaignMessage
(GET …/campaigns/{campaignId}/messages/{id}) are gone: the gateway no longer
serves that route. Use getSmsCampaignMessages / getEmailCampaignMessages
(the list), 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.
_ = try await hub.membership.saveMembershipTrigger([
"trigger": [
"type": "Membership",
"name": "welcome first",
"when": "OnRegistered",
"isEnabled": true,
"order": 1,
"breakOnError": true,
"action": ["type": "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 covers every
Code Hub endpoint (38); the five cloud-function routes list the functions an
integration may call, their aliases and versions, invoke one by hand, give a
sample envelope for a local run and the last runs of a trigger or task —
getCodeFunctions, getCodeFunctionQualifiers, invokeCodeFunction,
getCodeFunctionEventSample, getCodeFunctionRuns.
let functions = try await hub.code.getCodeFunctions(["integrationId": "int_1", "prefix": "orders-"])
let result = try await hub.code.invokeCodeFunction([
"functionRef": ["integrationId": "int_1", "functionId": "orders-on-update", "qualifier": "prod"],
"payload": ["orderId": "6ac54a59212c29912eaddd81", "dryRun": true],
"invocationType": "Sync",
])Details and the envelope the function receives: docs/hub/code.md.
NORBIX_PROJECT_ID=proj_123
NORBIX_API_KEY=sk_live_...
NORBIX_BEARER_TOKEN=...
NORBIX_ACCOUNT_ID=acc_456 # optional: no call needs it
NORBIX_API_URL=https://api.norbix.ai
NORBIX_HUB_URL=https://hub.norbix.ai
NORBIX_API_VERSION=v3
NORBIX_HUB_VERSION=v3
NORBIX_REGION=nb-eu-germanyYou can also build a config explicitly:
import NorbixCore
import NorbixApi
let cfg = try NorbixConfig.fromEnvironment(target: .api)
let client = NorbixApiClient(config: cfg)Every failure is a NorbixError from NorbixCore:
import NorbixCore
do {
_ = try await client.database.find(["collectionName": "orders"])
} catch let error as NorbixError {
switch error.code {
case "NORBIX_NOT_AUTHENTICATED": /* re-login */ break
case "NORBIX_ACCOUNT_SCOPE_REQUIRED": /* set accountId (no built-in call needs it) */ break
case "NORBIX_NETWORK_ERROR": /* retry */ break
default: print(error.localizedDescription)
}
// httpStatus / errorCode are the names every Norbix SDK uses.
// status / code are the same values, kept for older code.
print(error.httpStatus, error.errorCode, error.message)
for item in error.errors {
print(item.errorCode ?? "", item.fieldName ?? "", item.message ?? "")
}
print(error.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 Database calls follow rules the gateway checks (bulk writes with an empty
filter need "allRecords": true, $ operators in an update body are refused,
one schema-trigger copy per env) — the rules and each refusal's error code are
in docs/database-rules.md.
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 catch. downloadData (file download, the public file link) answers
with raw bytes rather than a document, is not JSON, and is unchanged.
API/Hub wrappers, tests, and module docs are generated by the SDK maintenance workflow.
swift test
swift buildThe 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.
Push to main, next, or beta:
- run CI
- semantic-release calculates version from commits
- create git tag + GitHub release
Swift Package Manager consumers get new versions from git tags/releases.
client.ai (NorbixApi) 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 dictionary.
let turn = try await api.ai.startEndUserChatTurn(["sessionId": sessionId, "message": "What can you do?"])
let entries = try await api.ai.getEndUserChatEntries(["SessionId": 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.
_ = try await hub.account.updateProjectLegalDocuments(["projectId": projectId, "termsMarkdown": "# Terms"])
_ = try await hub.account.updateProjectExposeLegal(["projectId": projectId, "exposed": true])
_ = try await hub.account.updateProjectExposeBrand(["projectId": projectId, "exposed": true])
_ = try await hub.account.updateProjectExposeAuth(["projectId": projectId, "exposed": true])
let terms = try await api.publicProjects.getPublicProjectLegal(projectId: projectId, kind: "terms")Tables and the MCP example: docs/hub/account.md, docs/api/public_projects.md.