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

Endpoint Scope Permission What it does
GET /me read Describe the token and who it acts as
GET /orgs read List the organizations this token may address
GET /orgs/{org}/apps read apps.read List the applications in an organization
POST /orgs/{org}/apps write apps.write Create an application with its three environments
GET /apps/{app} read apps.read Read one application
PATCH /apps/{app} write apps.write Update an application's settings
GET /apps/{app}/keys admin keys.read List ingest keys
POST /apps/{app}/keys admin keys.write Mint an ingest key
DELETE /apps/{app}/keys/{id} admin keys.write Revoke an ingest key
GET /apps/{app}/install read apps.read Install snippets for a framework

GET /me

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

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

{ "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:

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

{
  "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:

{ "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.

Ingest keys

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

Kind Prefix May
public spm_pub_<env>_ send events, evaluate flags — safe in a browser bundle
secret spm_sec_<env>_ the above, plus server-sent context.ip and context.userAgent
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
{
  "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 snippets show 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.

?framework=nextjs|react|node|html|express|hono   (default nextjs)
?environment=production|staging|development      (default production)
{
  "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 snippets mirror 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.