Skip to content
norbix-codePublic

About

Norbix sdk for IOS(Swift)

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

sdk-swift

NorbixSwift

CI Release Swift License

Official Swift SDK for Norbix. The SDK ships as two separate libraries that mirror the two Norbix planes:

  • NorbixApi — project-scoped data operations (default https://api.norbix.ai)
  • NorbixHub — project / account configuration operations (default https://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, code, database, echo, files, logs, membership, payments, publicProjects
NorbixHubClient accessToken, account, ai, apiKeys, auth, code, database, echo, email, environments, files, integrations, logs, membership, notifications, payments, 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.

Install (Swift Package Manager)

In Xcode:

  1. File -> Add Packages...
  2. Use URL: https://github.com/norbix-code/sdk-swift
  3. Pick a version rule (recommended: Up to Next Major)
  4. Add NorbixApi and/or NorbixHub to 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"),
    ]
)

Quickstart — API

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()

Quickstart — Hub

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()

Logs — write your own lines, read them on the Hub

_ = try await client.logs.writeLogs(["entries": [[
    "level": "error",                    // information (default) | warning | error
    "title": "Payment failed",           // 1–200 characters
    "eventCode": "order:payment_failed", // stored as app:order:payment_failed
    "correlationId": "checkout-42",
    "meta": ["orderId": "ord_1"]
]]])                                     // { accepted } — queued, returns at once

let page = try await hub.logs.getLogs(["level": "Error", "pageSize": 20])

A 429 CM-ERRORS-LOGGING-003 means the project's per-minute log quota is used up; wait NorbixError.retryAfterSeconds (from the gateway's Retry-After header) before sending again. After saving a logging integration, call hub.logs.testLoggingIntegration — nothing is delivered to it until its last test passed. Details: docs/api/logs.md, docs/hub/logs.md.

Keyless cloud connections

The AWS (S3, SES, Lambda) and Azure (Blob, Communication Services, Functions) save requests take authMode: Keyless (the gateway default when left out) or AccessKey. Keyless stores no secret: ask for the setup, run the script it returns in the cloud account, then save with the same integration id and the role ARN (AWS) or tenant + client id (Azure). A key mode needs acceptKeyRisk: true, or the save is refused (CM-ERRORS-INTEGRATIONS-034).

let setup = try await hub.integrations.getKeylessIntegrationSetup(["module": "files", "cloud": "aws"]) as? [String: Any]
// setup?["available"], setup?["unavailableReason"], setup?["subject"],
// setup?["audience"], setup?["script"], setup?["integrationId"],
// setup?["templates"] — templates: ready CloudFormation / Bicep / Terraform files for this integration

Self-hosted / on-prem deployments

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.

Multi-region support

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.

Selecting a region

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-Language

Switching at runtime

Both 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.

Per-call override

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")

Regional base URLs

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.ai
  • https://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.

Managing a project's regions (Hub)

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
])

Language

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 header

Wherever 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.

Real-world examples

1) Fetch recent orders (API)

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)
}

2) Login then call user endpoint (API)

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, "credential version:", auth.credentialVersion ?? "-")

let me = try await client.membership.getCurrentUser()
print(me)

3) Account Hub call (token only)

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])

4) Linked records with a typed answer (API)

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 Doe

More in API · Database → Linked records and Files by id.

Authentication

  • API key mode: apiKey or NORBIX_API_KEY
  • Bearer mode: bearerToken, NORBIX_BEARER_TOKEN, or client.login(...)
  • If both set, bearer token wins
  • If none set, the SDK throws NORBIX_NOT_AUTHENTICATED on the first authenticated call
  • Anonymous calls send no Authorization header, even when the client has a token: hub.account.createAccount, createTeamMemberFromInvitation, getAccountRegions, verifyAccount, and hub.regions.getAccountRegions

Preview a notification with its signed link (no sign-in)

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.

SMS campaigns to account users

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 it

The 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.

Retry a campaign's failed recipients

retryFailedEmailNotifications, retryFailedSmsNotifications and retryFailedPushNotifications (POST …/campaigns/{Id}/retry-failed, gateway 2.3.9) send the rows in the Failed state again. idempotencyKey (yours, at most 64 characters) is required: the same key again replays the first answer and sends nothing. The recipient rows now carry state, attempts[], deliveryEvents[] and deliveryOutcome; details and the optional selectors (notificationIds, categories, includeAmbiguous — may send a duplicate) in docs/hub/notifications.md.

let answer = try await hub.notifications.retryFailedSmsNotifications([
    "Id": "cmp_1",
    "idempotencyKey": "retry-2026-10-26-1",
]) as? [String: Any]
print(answer?["selected"] ?? 0, "queued;", answer?["skipped"] ?? [:])

Triggers: order and break on failure

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"],
    ],
])

A Membership trigger's Email, Push or SMS action can send to the person the event is about — the new user, the invited address, the deleted login — one recipient per firing. Send only recipientsSourceType: the subject fields (subjectAuthId, subjectEmail, subjectPhone, subjectLanguage) are filled by the gateway on the campaign the trigger starts and ignored on a save. Any other trigger type or a workflow step fails with CM-ERRORS-TRIGGERS-020. With no address, phone or device for the channel nothing is sent and the trigger is marked as needing attention.

_ = try await hub.membership.saveMembershipTrigger([
    "trigger": [
        "type": "Membership",
        "name": "welcome the new user",
        "when": "OnRegistered",
        "isEnabled": true,
        "action": [
            "type": "Email",
            "integrationId": "int_email",
            "templateId": "tpl_123",
            "deliverySettings": ["recipientsSourceType": "TriggerSubject"],
        ],
    ],
])

Trigger runs and execution identities

A trigger save and a scheduler task save take executionIdentityId: an id from hub.membership.getExecutionIdentities(), or nothing for the environment's default identity. hub.membership.createExecutionIdentity(["name": ..., "roleIds": ...]) makes one, and hub.triggers.checkTriggerExecutionIdentity(...) answers what an identity lacks for an action without saving. A Code or Marketplace action no longer takes runAsUserId. hub.triggers.getTriggerRuns(["triggerType": ..., "triggerId": ...]) lists one trigger's runs for 30 days; getTriggerRun(["id": ..., "triggerType": ...]) reads one, and the type is required. Template updates answer id and warnings.

Code — cloud functions

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 (70, with the 26 Workflows Beta routes); 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.

Workflows (beta; bugs may occur): hub.code has the 26 code/workflows routes (definitions, drafts and versions, runs, logs, resume); client.code on the Api client has the four end-user routes — getCodeWorkflowWaitingRuns, resumeCodeWorkflowRun and the two signed resume links, which need no session.

Environment configuration

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-germany

You can also build a config explicitly:

import NorbixCore
import NorbixApi

let cfg = try NorbixConfig.fromEnvironment(target: .api)
let client = NorbixApiClient(config: cfg)

Errors

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.

retryAfterSeconds (Int?) is the answer's Retry-After header in whole seconds — the number itself, or the seconds until the HTTP date it names (never below 0); nil when the header is absent or unreadable. A 429 carries it, for example CM-ERRORS-LOGGING-003 from client.logs.writeLogs when the project's per-minute log quota is used up:

do {
    _ = try await client.logs.writeLogs(["entries": entries])
} catch let error as NorbixError where error.httpStatus == 429 {
    try await Task.sleep(nanoseconds: UInt64(error.retryAfterSeconds ?? 60) * 1_000_000_000)
    // then send the entries again
}

A 401 CM-ERRORS-MEMBERSHIP-USERS-050 means the login's credentials changed (password changed or reset) after this session signed in: call login again. The sign-in answer carries the new credentialVersion (see docs/api/membership.md).

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.

Breaking change — a refused call now throws

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.

SDK maintenance

API/Hub wrappers, tests, and module docs are generated by the SDK maintenance workflow.

Development

swift test
swift build

Versioning

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 line Breaking: <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. The PR title check 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.

Release model

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.

End-user AI chat and project AI settings

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.

Project settings, public config, MCP endpoint and AI service users

  • hub.account: updateProjectAdminUrl, updateProjectLegalDocuments, updateProjectExposeLegal, updateProjectExposeBrand, updateProjectExposeAuth, getAdminPortalStructure, assignAdminPortalServiceUser.
  • api.publicProjects: getPublicProjectConfig, getPublicProjectLegal — no sign-in; sent with no Authorization header.
  • hub.account, AI service users: createAiServiceUser, listAiServiceUsers, rotateAiServiceUserKey, revokeAiServiceUserKey, deleteAiServiceUser.
  • hub.account, developer MCP endpoint: sendMcpMessage, openMcpStream, endMcpSession. They return an McpResponse that carries the session id from the Mcp-Session-Id answer 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.

About

Norbix sdk for IOS(Swift)

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages