Skip to main content

Ownership & Sharing

Looking for the product view?
The Help Center explains this surface as users see it: Sharing Apps.

Apps support team-based ownership and fine-grained sharing with access levels. The owner can change, and apps can be shared with multiple teams or users.

Ownership Model

  • Owner: A Principal (user or team) identified by ownerId
  • Natural ID: Apps with a slug use ownerId:slug as their natural ID
  • Transfer: Ownership can be changed to another team (requires write permission plus eligibility on the target team)

GET /api/apps/{id}/owner

Get the app's current owner (Principal with embedded User or Team).

Authentication: Required

Permissions Required: None (any user who can read the app)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
Get an App's ownerGET /api/apps/00000000-0000-4000-8000-000000000001/owner
The owner is a Principal; the matching User or Team is embedded.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/owner"
}
},
"id": "admin",
"userId": "admin",
"teamId": null,
"tenant": "acme",
"_embedded": {
"inf:user": {
"_links": {
"self": {
"href": "https://informer.example.com/api/users/admin"
},
"inf:memberships": {
"href": "https://informer.example.com/api/users/admin/memberships"
},
"inf:avatar-upload": {
"href": "https://informer.example.com/api/users/admin/_upload/{uploadId}/avatar-upload",
"templated": true
},
"inf:password": {
"href": "https://informer.example.com/api/users/admin/password"
},
"inf:feed": {
"href": "https://informer.example.com/api/users/admin/feed"
},
"inf:viewed-feed": {
"href": "https://informer.example.com/api/users/admin/_markFeedViewed"
},
"inf:superuser": {
"href": "https://informer.example.com/api/users/admin/superuser"
}
},
"permissions": {
"changeDomain": false,
"changeProfile": true,
"changePassword": true,
"delete": false,
"edit": true,
"reassign": true,
"superuser": true,
"enable": true,
"changeTheme": true,
"lock": true,
"setGlobalPermissions": true,
"manageLogin": true
},
"domain": "local",
"username": "admin",
"displayName": "acme Administrator",
"familyName": "Administrator",
"givenName": "acme",
"middleName": null,
"email": null,
"data": null,
"superuser": true,
"timezone": "America/New_York",
"settings": {
"log": {
"log": false,
"info": false,
"warn": false,
"debug": false,
"error": true,
"remote": false
}
},
"enabled": true,
"source": null,
"sourceId": null,
"lastViewedFeed": null,
"tenant": "acme",
"globalPermissions": {
"ai": false,
"jobCreation": true,
"viewAllTeams": true,
"viewAllUsers": true,
"painlessScriptCreation": true
},
"userFields": {},
"locked": false,
"lockedAt": null,
"loginAttempts": 0,
"passwordSetAt": "2026-01-15T16:20:00.000Z",
"passwordExpiresAt": "2026-01-15T16:20:00.000Z",
"forgotPasswordRequestTime": null,
"mfa": null,
"mfaConfig": {},
"aiMessage": null,
"aiConsent": false,
"hasSharedDatasources": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"initials": "EA",
"colorIndex": 4,
"avatarColor": {
"background": "#81C784",
"text": "#1B5E20"
},
"managesPassword": true
}
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

For team-owned apps, teamId is set and the team is embedded under inf:team instead.

Error Responses:

  • 404 Not Found - App doesn't exist or user lacks access

PUT /api/apps/{id}/owner

Change the app's owner to a different principal (typically a team).

Authentication: Required

Permissions Required:

  • app:write on the app
  • app:changeOwnerToTeam for the target: the tenant must have the AI capability and the caller needs team:assignOwnership (Designer or higher) on the target team

Payload:

The payload is the new owner's principal id; all other fields are stripped:

{
"id": "marketing"
}

An unknown principal id is rejected with 400 Bad Request.

Transfer ownershipPUT /api/apps/00000000-0000-4000-8000-000000000001/owner
The payload is the new owner's principal id. Transferring re-addresses the app: the naturalId becomes <newOwner>:<slug>.
Request body
{
"id": "marketing"
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk"
},
"inf:owner": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/owner"
},
"inf:draft": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/draft"
},
"inf:app-shares": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/shares"
},
"inf:app-tags": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/tags"
},
"inf:favorite": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/favorite"
},
"inf:app-tag": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/tags/{tagId}",
"templated": true
},
"inf:export-bundle": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_export"
},
"inf:cloud-publish": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_cloud-publish"
},
"inf:copy": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_copy"
},
"inf:print": [
{
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_print"
},
{
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_print",
"title": "Print to PDF"
}
],
"inf:migrate": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_migrate"
},
"inf:deploy": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_deploy"
},
"inf:app-tokens": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/tokens"
},
"inf:app-roles": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/roles"
},
"inf:app-dependencies": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/dependencies"
},
"inf:app-dependency": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/dependencies/{name}",
"templated": true
},
"inf:app-environment": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/environment"
},
"inf:app-environment-var": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/environment/{key}",
"templated": true
},
"inf:app-suggest-icon": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_suggest-icon"
},
"inf:app-modernize-manifest": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_modernize-manifest"
},
"inf:app-draft-diff": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/draft/_diff"
},
"edit": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_edit"
},
"inf:app-share": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/shares/{principalId}",
"templated": true
},
"inf:entrypoint": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/view/",
"title": "index.html"
},
"inf:library": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files",
"title": "App Files"
},
"inf:library-contents": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files/contents/index.html",
"title": "Raw Contents"
},
"share": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/view/?token=%7Btoken%7D",
"title": "Share Page"
}
},
"naturalId": "marketing:order-desk",
"isManaged": false,
"permissions": {
"assignTags": true,
"bind": true,
"changeOwner": true,
"copy": true,
"delete": true,
"edit": true,
"manage": true,
"purge": true,
"restore": true,
"run": true,
"share": true,
"switchRoles": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000001",
"type": "app",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "marketing",
"slug": "order-desk",
"description": "Track and fulfill open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": null,
"libraryId": "00000000-0000-4000-8000-000000000002",
"webhookSecret": "encrypted:iv:0000000000000000",
"dbRolePassword": null,
"deletedAt": null,
"editingId": null,
"deployedAt": null,
"editingSession": null,
"editingHeartbeatAt": null,
"editingLockedBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null,
"datasets": [],
"library": {
"naturalId": "admin:order-desk-files",
"connectionOwner": "admin",
"isLocal": true,
"permissions": {
"assignTags": true,
"changeOwner": true,
"configure": true,
"connect": true,
"edit": true,
"delete": true,
"write": true,
"sync": true,
"rename": true,
"share": true,
"editConnection": true
},
"id": "00000000-0000-4000-8000-000000000002",
"type": "local",
"name": "Order Desk Files",
"slug": "order-desk-files",
"description": null,
"ownerId": "admin",
"data": null,
"folderId": null,
"shared": false,
"assistantAccess": false,
"embedded": true,
"syncedAt": null,
"syncInterval": null,
"snapshotOfId": null,
"chatId": null,
"source": null,
"sourceId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"integration": null
},
"editing": null,
"contentModifiedAt": "2026-01-15T16:20:00.000Z"
}
Captured from the API examples test suite; ids and timestamps are normalized.

Important Notes:

  • The response is the full updated app with a Location header pointing at the new natural ID
  • Changing ownership updates the naturalId (the ownerId:slug pair), so the app's URL changes and the old natural ID stops resolving
  • All shares, tags, and other associations remain intact

Error Responses:

  • 400 Bad Request - Payload is not a known principal id
  • 404 Not Found - App doesn't exist or user lacks access
  • 403 Forbidden - User lacks write permission or cannot transfer to the target team

GET /api/apps/{id}/shares

Get all shares for an app (users and teams with access).

Authentication: Required

Permissions Required: None (any user who can read the app)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID

The response is a plain array (not a HAL collection). Each row joins the share with its principal's display details:

List an App's sharesGET /api/apps/00000000-0000-4000-8000-000000000001/shares
Each row is enriched with principal details: type (User or Team), display name, team icon/color or user avatar data.
Response · 200
[
{
"appId": "00000000-0000-4000-8000-000000000001",
"principalId": "marketing",
"accessLevel": 1,
"roles": [
"viewer"
],
"id": "marketing",
"teamId": "marketing",
"name": "Marketing",
"materialIcon": "campaign",
"icon": null,
"color": "purple",
"username": null,
"displayName": null,
"email": null,
"avatarUpdatedAt": null,
"type": "Team"
}
]
Captured from the API examples test suite; ids and timestamps are normalized.

Key Fields:

FieldDescription
principalIdID of the shared user or team
accessLevelAccess level integer (≥1; level-0 rows are filtered out)
rolesApp role ids assigned to this share (see Roles)
type"User" or "Team"
nameDisplay name (team name, user display name, or username)
materialIcon / icon / colorTeam chip styling (teams)
avatarUrl / colorIndex / initialsAvatar rendering data (users)

Error Responses:

  • 404 Not Found - App doesn't exist or user lacks access

GET /api/apps/{id}/shares/{principalId}

Get a specific share record (the raw AppShare row, same shape as the PUT response below).

Error Responses:

  • 404 Not Found - App, principal, or share doesn't exist

PUT /api/apps/{id}/shares/{principalId}

Create or update a share (grant access to a user or team).

Authentication: Required

Permissions Required: app:share (Publisher on the app's team, or superuser)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
principalIdstringPrincipal ID (username or team ID)

Payload Fields:

FieldTypeRequiredDefaultDescription
accessLevelintegerNo1Access level (must be ≥1, use DELETE to revoke)
rolesstring[]No[]Role IDs to assign (must match informer.yaml definitions). See Roles.

Unknown payload fields are stripped.

Share with a teamPUT /api/apps/00000000-0000-4000-8000-000000000001/shares/marketing
Upsert: repeat the PUT to change accessLevel or roles. roles entries must match ids defined in the manifest.
Request body
{
"accessLevel": 1,
"roles": [
"viewer"
]
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/shares/marketing"
}
},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"appId": "00000000-0000-4000-8000-000000000001",
"principalId": "marketing",
"accessLevel": 1,
"roles": [
"viewer"
],
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z"
}
Captured from the API examples test suite; ids and timestamps are normalized.

Upsert Behavior:

  • If a share already exists, updates accessLevel and roles
  • If no share exists, creates a new one
  • Setting accessLevel: 0 via PUT is invalid (use DELETE instead)

Error Responses:

  • 400 Bad Request - Invalid access level (must be ≥1)
  • 404 Not Found - App doesn't exist or user lacks access
  • 403 Forbidden - User lacks share permission

DELETE /api/apps/{id}/shares/{principalId}

Revoke access (delete a share).

Authentication: Required

Permissions Required: app:share (Publisher on the app's team, or superuser)

Revoke a shareDELETE /api/apps/00000000-0000-4000-8000-000000000001/shares/marketing
Responds 200 with an empty body.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

Behavior:

  • Removes the share record
  • The principal loses access (unless they're on the owning team or have another path to access)

Error Responses:

  • 404 Not Found - App doesn't exist
  • 403 Forbidden - User lacks share permission

GET /api/apps/{id}/data-access

Summarize the data access the app's manifest declares, for display in the sharing dialog ("people you share this with will also exercise this access").

Authentication: Required

Permissions Required: None (any user who can read the app)

Data access summaryGET /api/apps/admin%3Aorder-desk/data-access
Summarizes the manifest's access: whitelist for the sharing dialog. Returns null when the app has no manifest data access.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/data-access"
}
},
"datasets": [],
"queries": [],
"datasources": [],
"integrations": [],
"libraries": [],
"apis": [
{
"raw": "/api/jobs/*"
}
],
"hasAccess": true
}
Captured from the API examples test suite; ids and timestamps are normalized.

Response Shape:

FieldDescription
datasets / queries / datasources / integrations / librariesArrays of { id, name } for each resource the manifest's access: block whitelists. Entries gain notFound: true when the referenced resource no longer exists, and dataset entries gain hasFilter: true when the manifest pins a filter.
apisRaw API whitelist entries: { raw } for plain strings, { method, url } for object form
hasAccessWhether anything at all is whitelisted

Returns null (empty body) when the app has no library or its manifest declares no access: block. Note this summarizes the legacy/raw access: whitelist; dependency slots (dependencies:) are surfaced through GET /api/apps/{id}/dependencies instead.

Error Responses:

  • 400 Bad Request - The manifest exists but could not be loaded/parsed
  • 404 Not Found - App doesn't exist or user lacks access

Common Sharing Patterns

Share with a Team

// Grant read access to Marketing team
await PUT('/api/apps/analytics:sales-dashboard/shares/marketing', {
accessLevel: 1
});

// Verify the share
const shares = await GET('/api/apps/analytics:sales-dashboard/shares');
console.log('Shared with:', shares.map(s => s.name));

Transfer Ownership

// Transfer app to Marketing team
const updatedApp = await PUT('/api/apps/analytics:sales-dashboard/owner', {
id: 'marketing'
});

console.log('New natural ID:', updatedApp.naturalId);
// "marketing:sales-dashboard"

Check Data Access Before Sharing

const dataAccess = await GET('/api/apps/analytics:sales-dashboard/data-access');

if (dataAccess?.hasAccess) {
console.log('Datasets:', dataAccess.datasets.map(d => d.name));
console.log('Raw APIs:', dataAccess.apis);
}

await PUT('/api/apps/analytics:sales-dashboard/shares/marketing', {
accessLevel: 1
});

Share with Roles

// First, check what roles the app defines
const rolesResp = await GET('/api/apps/analytics:sales-dashboard/roles');
console.log('Available roles:', rolesResp.roles.map(r => r.id));

// Share with specific roles
await PUT('/api/apps/analytics:sales-dashboard/shares/john.doe', {
accessLevel: 1,
roles: ['viewer', 'approver']
});

Permission Levels Summary

OperationRequired role on the app's team
Read/RunRead access via ownership or a share
Update metadata (PUT /apps/{id})Designer (Magic Report) / Admin (full App)
Share / revoke sharesPublisher
Transfer ownershipapp:write plus Designer on the target team