# Yurbi API — Full Developer Documentation
> Reference for the Yurbi API — authenticate, manage users, configure apps, and refresh licenses on your own Yurbi server. JSON over HTTPS.
This is the complete, self-contained reference for the Yurbi API. Every endpoint is an HTTP `POST` that accepts and returns JSON and runs on your own self-hosted Yurbi server — no data leaves your infrastructure. Authenticate once with `DoLogin` to receive a session token, then pass that token on every other call. The base URL is your own instance; the path is `/api` on Linux/Docker and `/yurbi/api` on Windows. Source docs: https://developers.yurbi.com/
===============================================================
GETTING STARTED
===============================================================
# Introduction
Source: https://developers.yurbi.com/
## What the Yurbi API is
The Yurbi API lets your application drive a Yurbi instance programmatically:
authenticate users, create and manage users and groups, configure data security,
run reports, and embed dashboards directly in your product. It runs on **your
own Yurbi server** — no data leaves your infrastructure.
The API is intentionally simple:
- **One transport.** Every endpoint is an HTTP `POST` that accepts and returns JSON.
- **One base path.** All endpoints live under `https://your-yurbi-server.com/api/...`.
- **One auth model.** You log in once to get a *session token*, then pass that token
on every other call.
## What you can do with it
- **Embed analytics** — drop a Yurbi dashboard or report into your own app, branded
as yours, scoped to each customer's data. See the [Embedding guide](/guides/embedding/).
- **Single sign-on** — sign users in with no second login using a session token. See
[Single Sign-On](/guides/single-sign-on/).
- **Provision access** — create users, build security groups, and assign roles so each
customer sees only what they should.
- **Enforce isolation** — attach groups to AppShield data-security policies.
- **Automate operations** — configure email, instance settings, and licensing as part
of your own deployment pipeline.
## Where to go next
1. **[Quickstart](/quickstart/)** — make your first successful call in a few minutes.
2. **[Authentication & sessions](/authentication/)** — how tokens work and stay alive.
3. **[Conventions & errors](/conventions/)** — the patterns shared across every endpoint.
4. **[API Reference](/reference/)** — every endpoint, with live, runnable examples.
> The Reference page has a built-in "Try it": enter your server's Base URL and a
> token, edit any request, and send it live (or copy a ready-to-run cURL).
# Quickstart
Source: https://developers.yurbi.com/quickstart/
## 1. Log in to get a session token
Every call needs a session token. Get one by posting your credentials to
`DoLogin`. Replace the host with your own Yurbi server.
```bash
curl -X POST "https://your-yurbi-server.com/api/login/DoLogin" \
-H "Content-Type: application/json" \
-d '{
"bolForceLogin": true,
"isGuest": false,
"UserId": "admin",
"UserPassword": "your-password"
}'
```
A successful response has `ErrorCode: 0` and your token at
`LoginSession.SessionToken`:
```json
{
"ErrorCode": 0,
"ErrorMessage": "",
"LoginSession": {
"SessionToken": "LVPACMJFBMZKUHRIXMQSTUZUY",
"isGuestSession": false,
"SessionExpir": "2025-09-02T13:27:43.382837-04:00"
}
}
```
Bad credentials return `ErrorCode: 101` with "Login Failed - Username or Password
is invalid." and a null `LoginSession`.
## 2. Use the token on a call
Pass the token in the JSON body of every other request — it is **not** an HTTP
header. For example, list the users on the instance:
```bash
curl -X POST "https://your-yurbi-server.com/api/Contact/GetContactList" \
-H "Content-Type: application/json" \
-d '{ "sessionToken": "LVPACMJFBMZKUHRIXMQSTUZUY" }'
```
That's the whole pattern: **log in once, reuse the token.**
> **Linux/Docker vs Windows paths.** These examples use the Linux/Docker base
> `/api`. On Windows (IIS), Yurbi is served under `/yurbi`, so the path is
> `/yurbi/api/...` (e.g. `/yurbi/api/login/DoLogin`). See
> [Conventions](/conventions/#platform-where-yurbi-is-served), or just use the
> platform toggle on the [Reference](/reference/) page.
## 3. Try it without writing code
Open the [API Reference](/reference/), paste your Base URL and token into the bar
at the top, and every code sample updates with your values. Hit **Send** to run a
call live against your server, or copy the ready-to-run cURL.
## Next
- [Authentication & sessions](/authentication/) — keeping tokens alive, logging out.
- [Conventions & errors](/conventions/) — the rules shared by every endpoint.
- [Embedding guide](/guides/embedding/) — put a dashboard in your app.
# Authentication & sessions
Source: https://developers.yurbi.com/authentication/
Every Yurbi API call except [`GetInstallationID`](/reference/#get-installation-id)
requires a session token, sent as a field in the request body.
## Getting a token
[`DoLogin`](/reference/#do-login) exchanges credentials for a token:
```bash
curl -X POST "https://your-yurbi-server.com/api/login/DoLogin" \
-H "Content-Type: application/json" \
-d '{ "bolForceLogin": true, "isGuest": false,
"UserId": "apiuser", "UserPassword": "YOUR_PASSWORD" }'
```
The token is in `LoginSession.SessionToken`, and `LoginSession.SessionExpir` gives
the moment it lapses. Call this from your server so credentials never reach a
browser.
The response also describes the signed-in user: their groups, their application
roles, and the instance's licensed features. It is around 200 KB, because it
includes the interface phrase table.
## One session per user
**Yurbi maintains a single session per user account.** Each successful login
issues a new token and ends that user's previous session. A token from an earlier
login stops working and returns `ErrorCode 101`.
This shapes how an integration should be built:
- **Cache the token on your server and reuse it** across requests, rather than
calling `DoLogin` per request.
- **Give each integration its own Yurbi user.** A scheduled export, an embedded
dashboard and an interactive application should not share one account.
- **When embedding for end users, create a Yurbi user per end user.** Sharing one
account across several people means each new sign-in ends the previous person's
session.
## Session lifetime
A session lasts for the instance's `SESSION_TIMEOUT`, which is 20 minutes by
default and readable through [`GetAppSettings`](/reference/#get-app-settings).
The window is a rolling one: **every authenticated call moves the expiry forward**
to the time of the call plus the timeout. An integration that makes regular
requests keeps its own session alive.
Two endpoints manage the session explicitly:
- [`CheckSession`](/reference/#check-session) reports whether a token is still
valid. `ErrorCode 0` means valid; `ErrorCode 101` means expired or unknown.
- [`RefreshSession`](/reference/#refresh-session) extends a session without
performing any other work — useful for a dashboard left open with no traffic.
[`DoLogout`](/reference/#do-logout) ends a session immediately. Call it when a
short-lived script finishes.
## Handling expiry
An expired token shows up in one of two ways: a `204` response with an empty body,
or `ErrorCode 101`. Handle both by logging in again and retrying.
```
if response is empty (204) or ErrorCode == 101:
token = DoLogin(...) # replaces any earlier session
retry the request once
```
Because a fresh login ends the user's previous session, a retry loop should be
per-account rather than per-request: two workers sharing an account will
repeatedly evict each other.
## Permissions
The built-in `admin` account is a super-admin and bypasses application and group
permissions. Any other administrator must be granted a role on each application it
uses and added to the groups whose content it needs. See
[Permissions and the super-admin account](/conventions/#permissions-and-the-super-admin-account)
for what to grant a service account.
## Guest sessions
Set `isGuest` true to create a guest session for anonymous content. Guest sessions
are also returned by `DoLogin`, and `LoginSession.isGuestSession` identifies them.
For public content with no sign-in at all, enable anonymous view on the item and
use the public view URL instead — see
[Embed dashboards & reports](/guides/embedding/).
## Reserved accounts
The `scheduler` and `yurbi` accounts are reserved for internal services and cannot
sign in through the API.
# Conventions & errors
Source: https://developers.yurbi.com/conventions/
## Requests
Every endpoint in this reference is a **`POST`** carrying a JSON body, and every
authenticated call takes the session token as a **field in that body** rather than
a header:
```bash
curl -X POST "https://your-yurbi-server.com/api/Session/CheckSession" \
-H "Content-Type: application/json" \
-d '{ "sessionToken": "YOUR_SESSION_TOKEN" }'
```
### Platform: where Yurbi is served
| Deployment | API base |
|---|---|
| Linux / Docker | `https://your-server.com/api` |
| Windows (default) | `https://your-server.com/yurbi/api` |
| Windows (IIS root web) | `https://your-server.com/api` |
Endpoint paths are case-insensitive.
## Status codes
Yurbi reports application errors **in the response body with a 200 status**. The
HTTP status tells you whether the request reached the endpoint, not whether it
succeeded.
| Status | Meaning |
|---|---|
| `200` | The call reached the endpoint. Check the body for an error code. |
| `204` | Empty response. The session token was rejected, or the caller lacks permission for the object. |
| `404` | No such path. Check the endpoint name and the platform prefix. |
| `500` | A required parameter was missing from the body. |
A `204` with an empty body is the most common sign of an expired token. Confirm
with [`CheckSession`](/reference/#check-session).
## Error reporting
Most endpoints return an envelope with `ErrorCode` and `ErrorMessage`, where `0`
means success:
```json
{ "ErrorCode": 0, "ErrorMessage": "" }
```
Three variations appear across the API:
- **Per-item errors.** List endpoints return a bare array, and each item carries
its own `ErrorCode`. There is no wrapper object around the array.
- **Alternative field names.** Some endpoints use `ERROR_CODE` / `ERROR_MESSAGE`,
`error_code` / `error_message`, `returncode` / `message`, or `Code` / `Message`.
Each endpoint page shows the shape it returns.
- **`HasError` on report execution.** [`GetReportData`](/reference/#get-report-data)
reports failures through `HasError` and leaves `ErrorCode` at `0`. **When you run
a report, branch on `HasError`.**
### Error codes
| Code | Meaning |
|---|---|
| `0` | Success |
| `101` | Login failed, or the session has expired |
| `200` | No licence available for the requested action |
| `586` | The report does not exist |
| `9000` | Unexpected exception. The request was malformed or a required field was missing |
| `9001` | The endpoint could not complete the request |
| `9002` | The registered server could not be reached |
### Bare values
Some endpoints return a single value rather than an envelope, and **that value is
served as plain text, not JSON**. `JSON.parse()` will fail on these responses —
read the body as text instead.
```
POST /api/library/FavReport -> 0
POST /api/RegServers/TestConnection -> Passed
POST /api/Group/AddUser -> User Added Successfully.
POST /api/LicenseManager/GetInstallationID
-> 4446-4836-3034-3631-4452-3666
```
[`DoLogout`](/reference/#do-logout) returns `0`,
[`GetSQL`](/reference/#get-sql) returns the generated SQL, and
[`TestConnection`](/reference/#test-connection) returns `Passed` or a failure
message. Each endpoint page shows its exact response.
An empty array means either no matching records or a rejected token, so check the
session when an empty result is unexpected.
## Working with objects: fetch, modify, send
Endpoints that create, update or delete an object take the **whole object**, not
an ID. The pattern is the same throughout the API:
1. Fetch a starting object — a `New…` template when creating, or a `Get…` call
when changing or removing something that exists.
2. Modify the fields you care about.
3. Send the complete object back.
```
# create a user
POST /api/Contact/NewContact -> template
POST /api/Contact/SaveContact -> { user: }
# delete a user
POST /api/Contact/GetContactList -> find the user object
POST /api/Contact/DeleteContact -> { user: }
```
Building an object by hand tends to omit collections the endpoint expects, which
surfaces as `ErrorCode 9000`. Starting from a template avoids that, and keeps your
integration working as fields are added in later releases.
Create and update are the same call on most objects, distinguished by the
identifier: send `null` (or `""` for reports) to create, or an existing ID to
update.
## Permissions and the super-admin account
The built-in `admin` account is a **super-admin**. It bypasses application and
group permissions entirely, so every API call succeeds regardless of how content
is scoped.
**Every other administrator is subject to permissions**, including one you add to
the Administrators group. Before an integration user can work with an application
through the API, grant it a role on that application — Architect for App Builder
work, for example — and add it to the groups whose content it needs to reach. If
you register a new application, existing administrators will not see it until they
are granted a role on it.
This matters when moving from a proof of concept to production: work that
succeeded under `admin` can return empty results or `204` responses under a
purpose-made service account that has not been granted access.
## Response size
Responses are not paged. Several endpoints return substantially more than they
appear to:
- [`DoLogin`](/reference/#do-login) returns around 200 KB, most of it the
interface phrase table.
- [`GetApplicationList`](/reference/#get-app-list) and
[`GetAllSecurityGroups`](/reference/#get-all-security-groups) embed full user
records for everyone they touch.
- [`GetReportData`](/reference/#get-report-data) returns every row the report
produces. A detail report over a large table can return several megabytes.
Where volume matters, run reports built with a `TopN` limit or an aggregate rather
than fetching detail rows and reducing them in your own code.
## Field type codes
Report metadata describes each field with a short code, in `Fieldtype` and
`yurbitype`. The full list is available from
[`GetDataTypes`](/reference/#get-data-types).
| Code | Meaning |
|---|---|
| `cha` | Character |
| `num` | Numeric |
| `dat` | SQL datetime — the client applies a timezone offset |
| `dtz` | SQL datetime, no timezone conversion |
| `doz` | Date only, no timezone conversion |
| `udt` | Unix datetime |
| `tsp` | Oracle timestamp |
| `ddn` | Drill-down field |
| `lnk` | Link field |
| `flk` | File download link |
| `for` | SQL formula field |
| `cur` | Formatted as currency |
| `per` | Formatted as a percentage |
Date codes are not interchangeable. `dat` carries a time component and is offset
for the viewer's timezone; `doz` is a plain date and is returned exactly as
stored.
## Output types
A report's `OutputType` — and the `itemtype` returned in library listings —
identifies how the report is rendered. The current list is available from
[`GetOutputTypes`](/reference/#get-output-types).
| ID | Output |
|---|---|
| `0`, `1` | Data grid |
| `2` | Chart |
| `3` | KPI text |
| `4` | KPI gauge |
| `6` | Pie chart |
| `7` | Combo chart |
| `8` | Pivot grid |
| `9` | Tree map |
| `10` | Vector map |
| `11` | Skyline |
| `12` | Aggregate grid |
| `13` | Advanced pivot grid |
| `14` | Chart v2 |
Library listings return dashboards alongside reports, with `itemtype` `0`. Filter
them out before passing IDs to report endpoints.
## Roles
| ID | Role | Applies to |
|---|---|---|
| `0` | None | Group membership |
| `1` | Admin | Group membership |
| `2` | View | Group membership |
| `3` | Modify | Group membership |
| `4` | Delete | Group membership |
| `5` | Builder | Application assignment |
| `6` | Agent | Application assignment |
| `7` | Architect | Application assignment |
Roles 1–4 control what a user may do with content in folders scoped to a group.
Roles 5–7 are assigned per application through `UserApplications` on
[`SaveContact`](/reference/#save-contact) and consume a licence seat.
## Cross-origin requests
The API returns `Access-Control-Allow-Origin: *`, so browser-based applications
can call it directly. Because the session token travels in the request body,
authenticate on your server rather than exposing credentials in client-side code.
# Finding more endpoints
Source: https://developers.yurbi.com/finding-endpoints/
## What's documented here
This reference focuses on the calls a software vendor actually needs:
**authenticating users, provisioning them (users, groups, roles, AppShield
policies), discovering content, and rendering or embedding it.** That's the stable,
supported core.
Behind the scenes, Yurbi exposes an API for **every action in the interface** —
building reports, designing dashboards, scheduling delivery, managing data sources,
and more. Those calls exist, but most aren't part of a typical embed/provision
integration, so they're not documented here.
## How to discover any endpoint
If you need to automate something you can do in the Yurbi UI, you can see exactly
which call it makes using your browser's developer tools:
1. Open Yurbi in your browser and sign in.
2. Open **DevTools** (F12, or right-click → Inspect) and select the **Network** tab.
3. Filter to **Fetch/XHR**.
4. Perform the action in the Yurbi UI (e.g. favorite a report, save a dashboard).
5. Click the `/api/...` request that appears. The **Payload/Request** tab shows the
URL and JSON body; the **Response** tab shows what comes back.
6. Replicate that request from your own code, passing your `sessionToken` in the body.
Everything you find follows the same [conventions](/conventions/): `POST` + JSON,
`sessionToken` in the body, the `ErrorCode` / `ErrorMessage` envelope, and the
read-modify-write pattern for `Save*` calls.
## A caveat — and an offer
Undocumented endpoints are internal: they can change between versions without notice,
and their request/response shapes aren't guaranteed. The endpoints in this reference
are the ones we intend to keep stable for integrators.
If you find a call you'd like us to **document and support**, tell us what you're
trying to accomplish — we're happy to help, and to add it here if it fits the
embedding/provisioning story.
# API spec & tooling
Source: https://developers.yurbi.com/spec/
These files are generated automatically from the same source as this site, so they always match the published reference. Endpoints held back with a `draft` flag are excluded.
## Import into Postman
1. In Postman, choose **Import** and select the downloaded `yurbi-api.postman_collection.json` (or paste the URL `{{ site.url }}/yurbi-api.postman_collection.json`).
2. Open the collection's **Variables** tab and set:
- **`baseUrl`** — your Yurbi host, e.g. `https://your-yurbi-server.com`. On a
default Windows install add the `/yurbi` prefix: `https://your-yurbi-server.com/yurbi`.
- **`sessionToken`** — leave empty.
3. Run **Authentication → Log in** (`DoLogin`) with your credentials. The
collection captures the returned token into the `sessionToken` variable
automatically, and every other request is ready to send.
## Import into Insomnia / Swagger UI / code generators
Point any OpenAPI-aware tool at `{{ site.url }}/openapi.json`. The server URL in the spec is a placeholder — replace it with your own Yurbi host. The same file works with `openapi-generator` and similar tools to scaffold a typed client.
## For LLMs and answer engines
If you're feeding these docs to an AI assistant, the [`llms.txt`]({{ site.url }}/llms.txt) index and the full [`llms-full.txt`]({{ site.url }}/llms-full.txt) corpus are the most useful entry points — both are also generated from the published endpoints.
===============================================================
GUIDES
===============================================================
# Single Sign-On
Source: https://developers.yurbi.com/guides/single-sign-on/
## The short answer
We get asked constantly: *how do we do single sign-on with Yurbi?* For almost
every integration the answer is the **session-token flow** — you already
authenticate the user in your own app, so:
1. Call [`DoLogin`](/reference/#do-login) to get a session token.
2. Use that token to either **send the user into the full Yurbi interface**, or
**embed a specific dashboard or report** in your app.
No separate Yurbi login screen, no extra credentials for the user.
> Want your identity provider (e.g. Microsoft Entra ID) to authenticate users and
> have Yurbi log them in automatically — without your app calling `DoLogin`? That's
> a special case: see [Header-based SSO (advanced)](/guides/sso-header/). Most
> integrations don't need it.
## 1. Get a session token
Call `DoLogin` (do this **server-side** so credentials are never exposed in the
browser):
```bash
curl -X POST "https://your-yurbi-server.com/api/login/DoLogin" \
-H "Content-Type: application/json" \
-d '{ "bolForceLogin": true, "isGuest": false, "UserId": "user", "UserPassword": "password" }'
```
The token comes back in `LoginSession.SessionToken`. (For more on tokens, see
[Authentication & sessions](/authentication/).)
> **Create a Yurbi user for each of your end users.** Yurbi maintains one session
> per user account, so a new login ends that account's previous session. If several
> people share a single Yurbi account, each sign-in signs the others out. Provision
> users as part of onboarding — see
> [Multi-tenant provisioning](/guides/multi-tenant-provisioning/).
## 2. Option A — Sign users into the full Yurbi interface
Append the token to `sso.html` and the user lands inside Yurbi, already signed in.
By default they land on the **Dashboard**; add `h` to choose a starting view:
```
Dashboard (default): https://your-yurbi-server.com/sso.html?s={sessionToken}
Library: https://your-yurbi-server.com/sso.html?s={sessionToken}&h=1
Builder: https://your-yurbi-server.com/sso.html?s={sessionToken}&h=2
```
Use this as the target of a button or redirect in your app. (Path shown is
Linux/Docker; on a default Windows install use `/yurbi/sso.html`, and with the IIS
root web configured it's `/sso.html` — see
[Platform paths](/conventions/#platform-where-yurbi-is-served).)
## 3. Option B — Embed specific content
Use the same token to drop a single dashboard or report into your own page via an
iframe:
```
https://your-yurbi-server.com/embed.html?t=d&i={dashboardId}&s={sessionToken}
```
The [Embedding guide](/guides/embedding/) covers this in full — finding IDs,
iframes, and report prompts.
## 4. Keep the session alive
A token expires at its `SessionExpir` time, which defaults to 20 minutes after the
last call. The window rolls forward on every authenticated request, so an active
session generally sustains itself.
For a session that may sit idle — a dashboard left open on a screen, for example —
call [`RefreshSession`](/reference/#refresh-session) before the expiry to extend
it, and [`CheckSession`](/reference/#check-session) to test validity.
## Endpoints used
- [`DoLogin`](/reference/#do-login) — mint the token
- [`RefreshSession`](/reference/#refresh-session) — keep it alive
- [`CheckSession`](/reference/#check-session) — validate it
# Multi-tenant provisioning
Source: https://developers.yurbi.com/guides/multi-tenant-provisioning/
This guide covers the recommended structure for serving many customers from one
Yurbi instance, and the API calls that create it.
The shape is simple: **one security group per tenant, one library folder scoped to
that group, and a data security policy that constrains every query to the tenant's
own rows.** Users belong to exactly one tenant group, which is what keeps tenants
invisible to one another.
## Before you start
Two settings belong to the instance rather than to any tenant.
**Turn on Tenant Mode.** Under **Settings → Server Settings → Application
Settings**, enable Tenant Mode. It restricts Builder and Architect users to the
All Users group, so they cannot reach across tenants when saving or sharing.
Confirm it from the API with [`GetAppSettings`](/reference/#get-app-settings),
which returns `TENANT_MODE_ENABLED`.
**Keep shared folders on All Users only.** Any folder everyone should see — shared
templates, for example — carries the All Users group and nothing else. With Tenant
Mode on, users can view that content but cannot save, edit or delete within it.
**Give your provisioning account the access it needs.** Only the built-in `admin`
account bypasses application and group permissions. If you provision with a
purpose-made service account, grant it a role on each application it will assign
to users. See
[Permissions and the super-admin account](/conventions/#permissions-and-the-super-admin-account).
## Onboarding a tenant
### 1. Create the tenant's security group
```bash
curl -X POST "https://your-yurbi-server.com/api/Group/SaveSecurityGroup" \
-H "Content-Type: application/json" \
-d '{
"sessionToken": "YOUR_SESSION_TOKEN",
"group": {
"GroupId": null,
"GroupName": "Tenant A",
"GroupDescription": "All users for Tenant A",
"GroupStatus": 0, "AllUsers": [], "AllRoles": []
}
}'
```
The response carries the new `GroupId`. Keep it: the folder, the data tag and the
policy all reference it.
**Never create a group that spans tenants.** All Users is the only group tenants
have in common. Because each user belongs to just their own tenant group, the
people picker when scheduling a report, and the group picker when saving one, show
that user only their own colleagues.
### 2. Create the tenant's library folder
Start from [`NewLibraryFolder`](/reference/#new-library-folder), set `fname`, and
scope the folder by giving it a single permission entry pointing at the tenant
group:
```json
{
"sessionToken": "YOUR_SESSION_TOKEN",
"isShared": true,
"currentUserId": "",
"libraryfolder": {
"id": 0,
"fname": "Tenant A",
"isParent": false, "isHidden": false, "InheritPermissions": false,
"Permissions": [
{ "LeftID": "", "RightID": "12",
"RelationshipType": "fld_grp", "RelationType": "fld_grp",
"PermissionTypeEnum": 2 }
]
}
}
```
`RightID` is the tenant's `GroupId`. A folder scoped this way is visible only to
members of that group — it will not appear in
[`GetAllLibraryTree`](/reference/#get-all-library-tree) for anyone else, including
other administrators.
Within their folder, a tenant's Builder can save reports and share them with
colleagues, and nothing they do is visible to another tenant.
### 3. Create the data tag that carries the tenant's identity
A data tag holds a value that varies per group. Row-level security then compares a
column against that value, so one report serves every tenant.
```json
{
"sessionToken": "YOUR_SESSION_TOKEN",
"datatag": {
"ID": "", "Label": "tenantid", "TagGroup": "Tenancy",
"DataTypeEnum": 1, "isGroup": true, "isUser": false,
"isActive": true, "DefaultValue": "NONE", "Contacts": [], "Index": 0,
"SecurityGroups": [
{ "LeftID": "", "RightID": "12",
"RelationshipType": "tag_grp", "RelationType": "tag_grp",
"Option1": "TENANT-A",
"PermissionTypeEnum": 0 }
]
}
}
```
`RightID` is the tenant's group and `Option1` is that tenant's value. Create the
tag once, then add a `SecurityGroups` entry per tenant as you onboard them.
`DefaultValue` applies to anyone with no assigned value. Setting it to something
that matches no rows means a misconfigured user sees nothing rather than
everything.
**Alternative: profile tags.** Instead of a data tag you can put the value
directly on the user, in `Tag1` through `Tag4`, and reference it as `/#tag1#/` in
the constraint. That suits per-user values, and it can be set at user creation
time in the same call — see step 5.
### 4. Create the data security policy
The policy applies a constraint to an application and report type, comparing a
column to the tag value. Build it from
[`NewPolicy`](/reference/#new-policy) so its collections are initialised, then
save it:
```json
{
"sessionToken": "YOUR_SESSION_TOKEN",
"policy": {
"id": null,
"Name": "Tenant row-level security",
"Description": "Constrains every query to the caller's tenant",
"Constraints": [], "Groups": [], "Users": [], "isActive": false
},
"isDeepSave": true,
"groups": [], "users": []
}
```
In the constraint, compare the tenant column to the tag — `/#tenantid#/` for a
data tag, or `/#tag1#/` for a profile tag.
**Assign the policy to All Users.** Every account is then constrained, and the tag
decides what each one sees. Assigning to individual tenant groups also works, but
means remembering to attach each new group. Set `isActive` true once you have
verified the constraint.
Steps 1 to 4 are per tenant, and only step 3's group entry and step 1 repeat as
you add more.
### 5. Create each user
One call sets identity, credentials, profile tags and application access:
```json
{
"sessionToken": "YOUR_SESSION_TOKEN",
"withpin": true,
"user": {
"ID": null,
"LoginName": "jsmith",
"FirstName": "Jane", "LastName": "Smith",
"EmailAddress": "jsmith@tenant-a.example.com",
"Company": "Tenant A",
"Pin": "GENERATED_PASSWORD",
"AuthType": "PIN", "twofa": "none",
"Tag1": "TENANT-A",
"timezone": -5, "timezonename": "Eastern Standard Time",
"SecurityGroups": [],
"UserApplications": [
{ "ApplicationID": "1001", "ApplicationName": "Sales",
"ApplicationRoleID": "6", "ApplicationRoleName": "Agent",
"ApplicationRoleType": "0", "applicationUserDataSourceID": "" }
]
}
}
```
`UserApplications` grants application access, and the role there decides what the
user can do with data:
| Role | ID | The user can |
|---|---|---|
| Agent | 6 | View and run content |
| Builder | 5 | Build reports and dashboards |
| Architect | 7 | Build applications and report types |
These roles consume licence seats.
### 6. Add the user to their tenant group
```json
{
"sessionToken": "YOUR_SESSION_TOKEN",
"GroupId": "12",
"ContactId": "639234395265102835",
"RoleId": "3"
}
```
The group role decides what the user may do **with content in the tenant's
folder**: `2` View, `3` Modify, `4` Delete.
The two roles answer different questions. The application role is what a user can
do with data; the group role is what they can do with the tenant's saved content.
### 7. Confirm the result
[`GetContactById`](/reference/#get-contact-by-id) returns the user with their
groups and application assignments, and
[`GetSecurityGroupById`](/reference/#get-security-group-by-id) confirms
membership.
## Creating a user for an existing tenant
Once a tenant exists, adding a person is two calls:
```
SaveContact -> create the user, set Tag1, assign applications
AddUser -> add to the tenant group with a library role
```
## Sessions and end users
Yurbi maintains **one session per user account**. A new login ends that account's
previous session, so give every end user their own Yurbi account rather than
sharing one, and cache each user's token on your server instead of logging in per
page view. See [Authentication & sessions](/authentication/).
This is why provisioning a user per end user matters even when your application
handles its own authentication: the Yurbi account is what carries the tenant's
data constraint and the session behind an embed.
## Offboarding
Remove in the reverse order of creation, since deletes take the whole object:
```
GetContactList -> DeleteContact (each user)
GetAllLibraryTree -> DelFolder (the tenant folder)
GetAllDataTags -> SaveDataTag (drop the tenant's SecurityGroups entry)
GetAllSecurityGroups -> DeleteSecurityGroup (the tenant group)
```
Deleting users first releases their licence seats.
## Endpoints used
- [`SaveSecurityGroup`](/reference/#save-security-group) ·
[`DeleteSecurityGroup`](/reference/#delete-security-group) ·
[`AddUser`](/reference/#add-user)
- [`NewLibraryFolder`](/reference/#new-library-folder) ·
[`SaveLibraryFolder`](/reference/#save-library-folder) ·
[`DelFolder`](/reference/#del-folder)
- [`SaveDataTag`](/reference/#save-data-tag) ·
[`GetAllDataTags`](/reference/#get-all-data-tags)
- [`NewPolicy`](/reference/#new-policy) ·
[`SaveAppShieldPolicy`](/reference/#save-appshield-policy)
- [`NewContact`](/reference/#new-contact) ·
[`SaveContact`](/reference/#save-contact) ·
[`DeleteContact`](/reference/#delete-contact)
# Run reports from your app
Source: https://developers.yurbi.com/guides/running-reports/
Yurbi reports can be run through the API and the results rendered anywhere. This
guide covers the full sequence, including prompts, which is where most
integrations need the detail.
## The sequence
```
SearchReports find the report ID
GetReportMetadataById fetch the report's runtime object
ReplacePromptCollection supply values, if the report has prompts
GetReportData run it and read Columns + Data
```
The metadata object returned in step two is what step four executes. Pass it
through unchanged apart from prompt values.
## 1. Find the report
[`SearchReports`](/reference/#search-reports) matches on name and returns items
the calling user can see:
```bash
curl -X POST "https://your-yurbi-server.com/api/library/SearchReports" \
-H "Content-Type: application/json" \
-d '{ "sessionToken": "YOUR_SESSION_TOKEN", "searchstring": "Revenue" }'
```
Results include dashboards as well as reports. Dashboards have `itemtype` `0` and
are not valid report IDs, so filter them out. To browse rather than search, use
[`GetAllLibraryTree`](/reference/#get-all-library-tree) followed by
[`GetListByFolderID`](/reference/#get-list-by-folder).
## 2. Get the report metadata
```bash
curl -X POST "https://your-yurbi-server.com/api/Report/GetReportMetadataById" \
-H "Content-Type: application/json" \
-d '{ "sessionToken": "YOUR_SESSION_TOKEN", "ReportID": "1738259678",
"isTarget": false, "PreviousMetadata": null, "Criteria": null }'
```
The response has two parts: `Report`, the definition, and `Prompts`, an array of
any values the report asks for at run time. When `Prompts` is empty or null, skip
to step four.
An unknown ID returns `ErrorCode 586` inside `Report`.
## 3. Supply prompt values
Each prompt describes one input. The fields that matter are:
| Field | Purpose |
|---|---|
| `PromptField.DisplayFieldName` | What to label the input in your interface |
| `PromptField.Fieldtype` | The value's type — `num`, `cha`, `dat` and so on |
| `isBetween` | The prompt takes a range, so set both values |
| `isInList` | The value may be a comma-separated list |
| `isSkipable` | The prompt may be skipped by setting `isSkipped` |
| `LowValue` | **Where you set the value** |
| `HighValue` | The upper bound when `isBetween` is true |
**Set values on `LowValue` and `HighValue`.** Those are the fields the API reads.
To offer a picker rather than a free-text box, call
[`GetFieldValues`](/reference/#get-field-values) with the prompt object; it
returns the selectable values as `{key, val}` pairs.
Apply the values with
[`ReplacePromptCollection`](/reference/#replace-prompt-collection), sending the
prompts back in the order and length the metadata gave you — values are matched to
prompts by position:
```json
{
"Reportobj": { "...": "the metadata object from step 2" },
"Prompts": [
{ "...": "prompt 0", "LowValue": "1115", "HighValue": "" },
{ "...": "prompt 1", "LowValue": "2024-01-01", "HighValue": "2026-12-31" }
]
}
```
The response is the metadata with those values compiled into the report's
criteria. Pass that to step four.
Two notes on values:
- **Send explicit dates.** For a date prompt, `GetFieldValues` returns relative
expressions such as `[Today]` and `[Last Month]` for use in interface pickers.
Resolve them to a date before sending.
- **Applying one prompt at a time.** [`ReplacePrompt`](/reference/#replace-prompt)
applies a single prompt and returns updated metadata. It compiles only the
prompt you pass, so call it once per prompt, feeding each response into the next
call as `Reportobj`.
## 4. Run the report
```json
{
"sessionToken": "YOUR_SESSION_TOKEN",
"IsDrillDown": false,
"DrillDownCriteria": null,
"Reportobj": { "...": "metadata, with prompts applied" },
"TargetReportobj": null
}
```
**Check `HasError`, not `ErrorCode`.** On this endpoint a failed execution returns
`HasError: true` with the reason in `ErrorMessage`, while `ErrorCode` stays `0`.
```javascript
const res = await runReport(metadata);
if (res.HasError) {
throw new Error(res.ErrorMessage);
}
```
## 5. Read the results
`Columns` describes the shape and `Data` holds the rows, one object per row keyed
by column:
```json
{
"HasError": false,
"Columns": [
{ "headerText": "Customer", "key": "Customer_ID", "dataType": "number",
"yurbitype": "num", "dataFormatString": null, "columngroup": "" },
{ "headerText": "Tickets", "key": "Ticket_ID", "dataType": "number",
"yurbitype": "num", "dataFormatString": null, "columngroup": "" }
],
"Data": [ { "Customer_ID": 1115, "Ticket_ID": 10 } ],
"etime": "79"
}
```
Use `headerText` for column labels and `key` to read each row. `yurbitype` tells
you how to format the value — see
[Field type codes](/conventions/#field-type-codes). `etime` is the execution time
in milliseconds.
Rendering a table takes only `Columns` and `Data`; the remaining fields are
interface bindings used by Yurbi's own components.
```javascript
const rows = res.Data.map(row =>
res.Columns.map(col => row[col.key])
);
```
## Practical notes
**Results are not paged.** A report returns every row it produces, so a detail
report over a large table can return several megabytes. Where you need a summary,
run a report built with an aggregate or a `TopN` limit rather than fetching detail
rows and reducing them in your own code.
**Running without prompts.** Set `supressprompts` true on
[`GetReportData`](/reference/#get-report-data) to run a prompted report without
supplying values. The result is unfiltered by those prompts, which suits scheduled
extracts where the filtering happens downstream.
**Reuse metadata across runs.** The metadata object for a given report is stable,
so an application that runs the same report with different prompt values can fetch
it once and apply values per run.
**Row-level security still applies.** Data security policies constrain results by
the calling user, so run reports under the end user's own session rather than a
shared account if tenants must stay separated. See
[Multi-tenant provisioning](/guides/multi-tenant-provisioning/).
## Endpoints used
- [`SearchReports`](/reference/#search-reports) ·
[`GetListByFolderID`](/reference/#get-list-by-folder)
- [`GetReportMetadataById`](/reference/#get-report-metadata)
- [`GetFieldValues`](/reference/#get-field-values) ·
[`ReplacePromptCollection`](/reference/#replace-prompt-collection) ·
[`ReplacePrompt`](/reference/#replace-prompt)
- [`GetReportData`](/reference/#get-report-data)
# Row-level data security
Source: https://developers.yurbi.com/guides/data-security/
Row-level security in Yurbi has two parts. A **data tag** holds a value that
varies by group or by user. An **AppShield policy** applies a constraint that
compares a column to that value. Together they let a single report serve everyone
while returning only the rows each viewer is entitled to.
Nothing changes in the report itself. Constraints are applied when the report
runs, so they cover the API, the interface, embeds, exports and scheduled
delivery alike.
## When to use which
| Approach | Value lives on | Good for |
|---|---|---|
| **Data tag, group-scoped** | A security group | Tenants, regions, departments — anywhere a set of people shares one value |
| **Data tag, user-scoped** | A single user | Values that differ per person |
| **Profile tag** | `Tag1`–`Tag4` on the user record | Values you already hold in your own system and can set when creating the user |
Profile tags are often simplest for a multi-tenant deployment, because the value
can be set in the same call that creates the user.
## 1. Create a data tag
Start from [`NewDataTag`](/reference/#new-data-tag), then save it with a value for
each group:
```json
{
"sessionToken": "YOUR_SESSION_TOKEN",
"datatag": {
"ID": "", "Label": "tenantid", "TagGroup": "Tenancy",
"DataTypeEnum": 1, "isGroup": true, "isUser": false,
"isActive": true, "DefaultValue": "NONE", "Contacts": [], "Index": 0,
"SecurityGroups": [
{ "LeftID": "", "RightID": "12",
"RelationshipType": "tag_grp", "RelationType": "tag_grp",
"Option1": "TENANT-A", "PermissionTypeEnum": 0 },
{ "LeftID": "", "RightID": "13",
"RelationshipType": "tag_grp", "RelationType": "tag_grp",
"Option1": "TENANT-B", "PermissionTypeEnum": 0 }
]
}
}
```
Each `SecurityGroups` entry maps one group to one value: `RightID` is the group
ID, `Option1` is the value. Use `Contacts` the same way for per-user values.
`DefaultValue` applies to anyone with no assigned value. Set it to something that
matches no rows, so a user who has been missed sees nothing rather than
everything.
`Label` is the name the constraint refers to, written as `/#tenantid#/`.
Adding a tenant later means saving the tag again with an extra `SecurityGroups`
entry. Read the current tag with
[`GetAllDataTags`](/reference/#get-all-data-tags) first so you send back the
entries that already exist.
## 2. Create the policy
Build it from [`NewPolicy`](/reference/#new-policy) so its collections are
initialised, then save:
```json
{
"sessionToken": "YOUR_SESSION_TOKEN",
"policy": {
"id": null,
"Name": "Tenant row-level security",
"Description": "Every query is constrained to the caller's tenant",
"Constraints": [], "Groups": [], "Users": [], "isActive": false
},
"isDeepSave": true,
"groups": [], "users": []
}
```
A constraint targets one application and report type, and compares a column to
the tag:
```
customer_id = /#tenantid#/
```
Use `/#tag1#/` instead when the value is a profile tag on the user record.
## 3. Assign and activate
Assign the policy to **All Users** so every account is constrained and the tag
decides what each one sees. Assigning to individual tenant groups also works, but
means attaching each new group as you create it.
[`AddGroup`](/reference/#appshield-add-group) and
[`RemoveGroup`](/reference/#appshield-remove-group) adjust assignments on an
existing policy without rewriting it.
Set `isActive` true when you are ready to enforce the policy. Leaving it false
while you verify the constraint lets you save and review it first.
## Verifying a policy
The most direct check is to run a report as a user in the constrained group and
inspect the SQL:
1. Sign in as that user with [`DoLogin`](/reference/#do-login).
2. Fetch the report with
[`GetReportMetadataById`](/reference/#get-report-metadata).
3. Call [`GetSQL`](/reference/#get-sql) and confirm the WHERE clause contains the
tenant's value.
4. Run the report with [`GetReportData`](/reference/#get-report-data) and confirm
the rows.
Test with two tenants and a user in neither. The third case is the one that
catches a `DefaultValue` set too permissively.
## Notes
**Policies apply everywhere.** A constrained report returns the same restricted
rows through the API, the interface, an embed and a scheduled email. There is no
separate configuration per channel.
**Keep tenants in separate groups.** A user in two tenant groups may match more
than one tag value. One tenant group per user keeps the constraint unambiguous —
see [Multi-tenant provisioning](/guides/multi-tenant-provisioning/).
**The super-admin account is not constrained.** The built-in `admin` bypasses
these checks, so verify policies with an ordinary user account.
## Endpoints used
- [`NewDataTag`](/reference/#new-data-tag) ·
[`SaveDataTag`](/reference/#save-data-tag) ·
[`GetAllDataTags`](/reference/#get-all-data-tags) ·
[`DeleteDataTag`](/reference/#delete-data-tag)
- [`NewPolicy`](/reference/#new-policy) ·
[`SaveAppShieldPolicy`](/reference/#save-appshield-policy) ·
[`GetAllAppshieldPolicies`](/reference/#get-all-appshield-policies) ·
[`DeletePolicy`](/reference/#delete-policy)
- [`AddGroup`](/reference/#appshield-add-group) ·
[`RemoveGroup`](/reference/#appshield-remove-group)
# Building apps & reports
Source: https://developers.yurbi.com/guides/building-apps-and-reports/
## What this is for
Most integrations only *read* from Yurbi. This guide covers writing: creating
the semantic layer and the reports on top of it, entirely through the API.
Two situations need it:
- **Dynamic data sources.** A tenant's schema isn't known ahead of time, so
the report type has to be built at provisioning time rather than by hand in
App Builder.
- **Migration.** Bringing reports in from another BI tool. Those tools
usually store joins *per report*; Yurbi stores them in the **report type**,
so the report type has to exist before the report can be created.
## The two layers
| Layer | Holds | Written with |
|---|---|---|
| **Report type** | Tables, joins, the field tree | [`SaveAnyDbModule`](/reference/#save-anydb-module) |
| **Report** | Selected fields, filters, formulas, sort | [`SaveReport`](/reference/#save-report) |
A report names its report type **by name**, not by ID. Two report types
sharing a name inside one app makes every report on that name ambiguous, so
keep names unique.
## 1. Find the app
[`GetAnyDbModulesList`](/reference/#get-anydb-modules-list) returns every
AnyDB app with the two identifiers everything else needs:
```json
{ "ID": "17779219821", "ReportModule": { "ID": "1005", "Name": "AssetWorks Source ORACLE" } }
```
`ID` is the **RegServerID**, `ReportModule.ID` is the **ModuleID**.
## 2. Read before you write
[`GetAnyDbModule`](/reference/#get-anydb-module) returns the app's report
types. Read it first, always — for two reasons:
- to compute the next free `RepTypeID` (`max + 1`)
- to snapshot the current state, because the write is destructive
Look at `ReportTypes[].SQLTables` to see what tables an app already has.
`DBSQLTableList.AllDbtables` is live introspection of the database server and
comes back empty when the app has no live connection.
> These responses get large. An app with a couple of hundred tables and their
> columns runs to tens of megabytes.
## 3. Build the report type
A report type is `SQLTables` plus `FieldTree`.
### Tables and joins
Each table carries a single `join` describing how it attaches to the model:
```json
{
"tablename": "UNIT_MAIN",
"tableowner": "",
"tablealias": "UNIT_MAIN",
"join": {
"table1": "VIEW_ALL_WORK_ORDERS", "field1": "UnitNo",
"table2": "UNIT_MAIN", "field2": "UNIT_NO",
"jointype": "Inner Join", "joinop": "=", "filter": "", "index": 1
}
}
```
`table1` is the **parent** — the table already in the model. `table2` is the
one being attached. One table (the root) has an empty join.
Because each table has exactly one join, **the join model is a tree, not a
graph**. Every table has one parent. That rules out:
- the same pair of tables joined on two column pairs (composite keys)
- a table reachable from two different parents
- cycles
`jointype` is `"Inner Join"`, `"Left Join"` and so on — mixed case, no
`OUTER`. `joinop` is a bare `"="` with no padding. `filter` is appended to the
join clause verbatim if set.
`tableowner` should match whatever the app already uses. Many AnyDB apps
register tables with an empty owner even when the underlying database has a
schema — check `SQLTables` on an existing report type rather than assuming.
### The field tree
`FieldTree` is the field picker: folder nodes (`nodetype: "G"`) with field
nodes (`nodetype: "F"`) beneath them.
`flevel` is a colon-delimited materialised path made of **the parent's path
plus the node's own id**:
```
id 0 flevel "0" G VIEW_ALL_WORK_ORDERS (root folder)
id 1 flevel "0:1" F WorkOrderNo
id 2 flevel "2" G UNIT_MAIN
id 3 flevel "2:3" F Make
```
Two things that are easy to get wrong:
- `tablealias` on a field node is **its own** table's alias.
- `parenttablealias` is the **root table of the whole report type**, the same
value on every field node — not the field's immediate join parent.
`idx_order` starts at 1.
Common `datatype` codes: `cha` text, `num` numeric, `dat` timestamp,
`doz` date-only. The date codes are not interchangeable — see
[Dates](#dates-pick-the-right-code) below.
### Writing it
[`SaveAnyDbModule`](/reference/#save-anydb-module) **deletes and re-inserts
every report type in the payload**. Report types you leave out are untouched,
so send only the ones you are creating:
```json
{
"sessionToken": "…",
"anydbmodule": {
"ReportTypes": [ { /* just the new one */ } ],
"DBSQLTableList": { "AllDbtables": [] },
"isExcelCSV": false,
"ApiEndpointList": null
}
}
```
Posting a whole 16 MB module back means deleting and re-inserting thousands
of rows that were fine.
It returns `1` on success. Errors are swallowed and returned as `0`, so
**verify by reading the module back** rather than trusting the return value.
## 4. Build the report
Start from [`NewReport`](/reference/#new-report) so you inherit the current
defaults, then add fields.
The reliable way to add a field is to read the report type's tree with
[`GetReportTree`](/reference/#get-report-tree) and pass nodes to
[`NewField`](/reference/#new-field), which fills in derived values like
`RenamedField` (the SELECT alias) rather than making you reproduce the rules.
### Filters
Put structured criteria on the field and leave the compiled strings empty —
`SaveReport` compiles them:
```json
{
"DisplayFieldName": "Status",
"Criteria": [
{ "Index": 0, "Field": "Status", "Op": "inlist",
"Value1": "D,O", "Value2": "", "Logical": "And", "cond": "is" }
],
"SearchCriteria": ""
}
```
`Criteria.Field` must be the field's **display name**. It is the key binding a
criteria back to its field on load; if it doesn't match, the criteria is
orphaned and rebuilt from a compiled string, which changes the operator's
casing and breaks the criteria editor.
Operators are lowercase: `=`, `<`, `>`, `<>`, `between`, `inlist`, `like`,
`not like`, `null`, `empty`. `cond` is `is` or `isnot`. Multi-value lists are
comma-separated in `Value1` — values containing a comma will be split.
### Formulas
`Formula` holds raw SQL in the target dialect. `?` is replaced with the
field's own column, alias-qualified:
```
Formula: "ROUND((SYSDATE - ?), 0)"
becomes: ROUND((SYSDATE - "VIEW_ALL_WORK_ORDERS"."OpenDate"), 0)
```
Reference a *different* column by qualifying it explicitly with its table
alias — `"UNIT_MAIN"."MAKE"`. Quoting follows the platform (`"x"` on Oracle
and PostgreSQL, `[x]` on SQL Server).
Set `Fieldtype` to what the formula **returns**, not what it reads.
`ROUND(SYSDATE - "OpenDate", 0)` reads a date and returns a number, so it is
`num`.
### Aggregates
There is no aggregate flag. Put an aggregate function in `Formula` and the
engine detects it and groups every non-aggregated column automatically:
```json
{ "DisplayFieldName": "Ticket Count", "Formula": "count(?)", "Fieldtype": "num" }
```
Recognised: `sum(`, `avg(`, `min(`, `max(`, `count(`, `stddev(`. A formula
that is a subquery will not be treated as an aggregate.
### Saving
Build the object you save from [`NewReport`](/reference/#new-report) rather than
assembling it from scratch. The template initialises collections such as
`linkreports` that the save expects to be present; a hand-built object that omits
them returns `ErrorCode 9000`.
[`SaveReport`](/reference/#save-report) branches on `ReportID`:
- **empty string** → create; the server mints an ID and returns it
- **existing ID** → update
Sending an ID that doesn't exist fails with `9001 Permission Denied` — the
permission check runs against a report that isn't there. Never invent an ID.
`folderid` must be a library folder the user can modify, or the save fails
with `Folder Permission Denied`. Get candidates from
[`GetAllLibraryTree`](/reference/#get-all-library-tree); its real tree is
nested under a `rootnode` key on each entry, and `isUserAdmin`/`isUserModify`
tell you what's writable.
## 5. Validate before you commit
Two endpoints run the pipeline without saving anything:
- [`GetSQL`](/reference/#get-sql) takes a whole report object and returns the
SQL it would generate.
- [`ReportProcUI`](/reference/#report-proc-ui) compiles criteria and totals
and hands the object back.
`GetSQL` does **not** compile criteria, so a freshly built object produces SQL
with no `WHERE` clause. Chain them — `ReportProcUI` first, then `GetSQL` — to
preview the whole statement.
This is the cheapest possible feedback loop: build the object, look at the
SQL, fix it, and only then write.
## Dates: pick the right code
`dat` and `doz` are both dates, and they behave differently:
| Code | Renders as | Note |
|---|---|---|
| `dat`, `dtz`, `tsp` | `'2024-12-01 00:00:00'` | timestamp; the client applies a browser timezone offset |
| `doz`, `idt`, `dpo` | `'2024-12-01'` | date only, no offset |
For values that are literal dates with no timezone, `doz` reproduces them
exactly. `dat` is right when time of day genuinely matters.
Either way, check period boundaries: with `doz`, `Between '2024-12-01' and
'2024-12-31'` excludes times later on the 31st.
## Putting it together
```
GetAnyDbModulesList find the app
GetAnyDbModule read current state, snapshot, next RepTypeID
↓
SaveAnyDbModule create the report type (send only the new one)
GetAnyDbModule verify it was written
↓
GetReportTree read the new field tree
NewReport + NewField assemble the report
ReportProcUI → GetSQL preview the SQL, no writes
↓
GetAllLibraryTree pick a writable folder
SaveReport ReportID: "" → returns the new ID
```
## Things worth knowing
- **Report types are referenced by name.** Re-creating one with the same name
but a different join graph breaks existing reports on it. Check for an
existing match before creating.
- **`SaveAnyDbModule` returns `0` on error**, with no message. Verify by
reading back.
- **Only the tables a report uses are joined.** Selecting fields from two
tables of a ten-table report type generates a two-table query, and
intermediate tables needed to connect them are pulled in automatically. A
broad report type therefore costs a narrow report nothing — one report type
can serve many reports.
- **Snapshot before writing.** `GetAnyDbModule` output is your rollback.
- **Reports can be removed.** [`DelReport`](/reference/#del-report) deletes a
report by ID and returns `0` on success, which makes it straightforward to roll
back an automated build that produced the wrong result.
- **Grant your integration user access to the app.** Only the built-in `admin`
account bypasses application permissions. Any other account needs an Architect
role on the application before these calls will see it.
# Embed dashboards & reports
Source: https://developers.yurbi.com/guides/embedding/
Yurbi embeds through the same session-token system as the rest of the API. The
flow: get a token, build an embed URL with it, drop that URL in an iframe.
Optionally log users straight into the full Yurbi interface.
## 1. Get a session token
Call [`DoLogin`](/reference/#do-login) to get a token (see
[Authentication & sessions](/authentication/)):
```bash
curl -X POST "https://your-yurbi-server.com/api/login/DoLogin" \
-H "Content-Type: application/json" \
-d '{ "bolForceLogin": true, "isGuest": false, "UserId": "user", "UserPassword": "password" }'
```
The token comes back in `LoginSession.SessionToken`.
> **One Yurbi user per end user.** Yurbi maintains a single session per account, so
> a new login ends that account's previous session. Embedding for several people
> through one shared account means each page load signs the previous viewer out.
> Provision a user per end user, and cache each user's token on your server rather
> than logging in on every page view.
## 2. Build the embed URL
Append the token to an embed URL. Use `t=d` for a dashboard, `t=r` for a report,
and `i=` for the item's ID:
```
Dashboard: https://your-yurbi-server.com/embed.html?t=d&i={dashboardId}&s={sessionToken}
Report: https://your-yurbi-server.com/embed.html?t=r&i={reportId}&s={sessionToken}
```
Place either URL in an `