SaaS Pro Max
Sign inStart free
Documentation menu

Identity, organizations, applications and keys

Everything an agent needs before it can send its first event. See README.md for authentication, scopes, errors and pagination.

Endpoints#

EndpointScopePermissionWhat it does
GET /meread—Describe the token and who it acts as
GET /orgsread—List the organizations this token may address
GET /orgs/{org}/appsreadapps.readList the applications in an organization
POST /orgs/{org}/appswriteapps.writeCreate an application with its three environments
GET /apps/{app}readapps.readRead one application
PATCH /apps/{app}writeapps.writeUpdate an application's settings
GET /apps/{app}/keysadminkeys.readList public and secret keys
POST /apps/{app}/keysadminkeys.writeCreate a public or secret key
DELETE /apps/{app}/keys/{id}adminkeys.writeRevoke a public or secret key
GET /apps/{app}/installreadapps.readInstall tracking code for a framework

GET /me#

The first call to make when you do not know what a token can do.

JSON
{
  "token": { "id": "9f2c…", "kind": "personal", "scopes": ["read", "write"] },
  "user": { "id": "u_1", "email": "ada@example.com", "name": "Ada" },
  "organizations": [{ "id": "o_1", "slug": "acme", "name": "Acme", "role": "owner" }],
  "permissions": ["org.read", "apps.read", "analytics.read", "analytics.write", "…"]
}

permissions is already intersected with the token's scopes, so it is exactly what this token may do — not what its owner may do in the console.

GET /orgs#

JSON
{ "data": [{ "id": "o_1", "slug": "acme", "name": "Acme", "role": "owner" }], "nextCursor": null }

An organization token lists exactly one row.

Applications#

GET /orgs/{org}/apps and GET /apps/{app} return the same shape:

JSON
{
  "id": "a_1",
  "slug": "site",
  "name": "Marketing site",
  "platform": "web",
  "url": "https://example.com",
  "timezone": "Europe/Paris",
  "modules": ["analytics", "people", "errors", "metrics", "flags"],
  "retentionDays": 365,
  "allowedOrigins": ["https://example.com"],
  "connector": {
    "status": "healthy",
    "capabilities": ["overview", "users", "controls"],
    "version": "demo-0.1.0",
    "lastHealthAt": "2026-09-03T09:12:44.117Z",
    "url": "https://app.example.com/api/spm/connector"
  },
  "organization": { "id": "o_1", "slug": "acme", "name": "Acme" },
  "environments": [
    { "id": "e_1", "slug": "production", "name": "Production" },
    { "id": "e_2", "slug": "staging", "name": "Staging" },
    { "id": "e_3", "slug": "development", "name": "Development" }
  ]
}

modules is the list to check before calling anything else: an endpoint whose module is off answers 403, and the assistant simply does not offer the tool.

connector#

null unless the application has a signed connector configured. When present:

Field Meaning
status unconfigured, configured, healthy, unhealthy or disabled
capabilities What the tenant handler answered on its last /health, a subset of overview, users, controls, prompts, agent-context, communications
version Version string the tenant reported, or null
lastHealthAt When that probe answered, or null
url Base URL of the tenant handler — only present for a token holding apps.write

status is the verdict of the last probe, not a live check: a healthy connector whose lastHealthAt is hours old has not been asked since. The url is where signed 60-second tokens are sent, so it is omitted for a read-only token rather than returned as null — the field is simply absent.

POST /orgs/{org}/apps#

JSON
{
  "name": "Marketing site",
  "slug": "site",
  "platform": "web",
  "url": "https://example.com",
  "modules": ["analytics", "errors"]
}

Only name is required; the slug is derived from it. The three environments are created with the application. A slug already used in that organization answers 409 conflict.

Enabling ai requires at least one data module — the assistant with nothing to read is a configuration error, not an empty dashboard.

PATCH /apps/{app}#

Send only what changes:

JSON
{ "name": "Site", "timezone": "UTC", "modules": ["analytics"], "allowedOrigins": ["https://example.com"], "retentionDays": 90 }

Origins are normalised to their origin form (https://example.com/pricing becomes https://example.com) and de-duplicated; * is kept as-is. Anything that does not parse as a URL is a 400. An empty body is a 400 rather than a silent no-op.

Public and secret keys#

These are the keys your application sends events with — not access tokens. Two kinds:

KindPrefixMay
publicspm_pub_<env>_send events, evaluate flags — safe in a browser bundle
secretspm_sec_<env>_the above, plus server-sent context.ip and context.userAgent
Shell
curl -X POST -H "Authorization: Bearer $SPM_TOKEN" -H 'content-type: application/json' \
  -d '{"kind":"public","environment":"production","label":"marketing site"}' \
  https://saaspro.dev/api/v1/apps/acme/site/keys
JSON
{
  "id": "k_1",
  "kind": "public",
  "environment": "production",
  "label": "marketing site",
  "prefix": "spm_pub_prod",
  "last4": "9f2c",
  "key": "spm_pub_prod_xxxxxxxxxxxxxxxxxxxxxx"
}

key is present in this response and nowhere else, ever. GET returns the prefix and last four characters only. DELETE revokes; revoking twice answers 404.

GET /apps/{app}/install#

Optional privacyMode=persistent|ephemeral defaults to persistent. Ephemeral instructions support html, react and nextjs only; other frameworks return 400. The response includes privacyMode. Ephemeral examples start consent pending and return no secret-key prefix or server instrumentation. All tracking code shows placeholders or stored public-key prefixes, which must be replaced with a full key.

Copy-paste instructions for one framework, using this application's real key prefix and host.

Text
?framework=nextjs|react|node|html|express|hono   (default nextjs)
?environment=production|staging|development      (default production)
JSON
{
  "framework": "nextjs",
  "environment": "production",
  "publicKeyPrefix": "spm_pub_prod",
  "secretKeyPrefix": "spm_sec_prod",
  "host": "https://saaspro.dev",
  "docsUrl": "/docs/sdk/recipes/nextjs-app-router",
  "snippets": [{ "title": "Install", "language": "bash", "code": "npm install @saaspro/react @saaspro/node" }]
}

The tracking code mirrors docs/sdk exactly — same packages, same option names, same exported symbols — so they can be pasted verbatim. They contain the key prefix, never a key: mint one with POST /keys and put the value in an environment file.

Telemetry usage and controls#

GET /apps/{app}/telemetry?env=production returns the current policy, 30 UTC days of usage and the latest 50 budget alerts. GET /apps/{app}/telemetry/cardinality returns bounded label/property diagnostics with signal-specific permissions. Both require apps.read. POST /apps/{app}/telemetry accepts {expectedRevision, policy} and requires apps.write; changing notification channel targets additionally requires metrics.write. See Telemetry budgets for accounting, sampling, cardinality and retry semantics.