Management API
One bearer token reads and writes everything the console shows: applications, ingest keys, analytics, events, funnels, people, errors, metrics, feature flags, revenue, destinations, campaigns, and the AI assistant.
Base URL: https://saaspro.dev/api/v1. Machine-readable description: openapi.json, also
served live at GET /api/v1/openapi.json without authentication.
curl -H "Authorization: Bearer spm_pat_xxxxxxxx" \
https://saaspro.dev/api/v1/orgs
Reference
| Group | Pages |
|---|---|
| Identity, organizations, applications, ingest keys | applications.md |
| Analytics, events, funnels, goals | analytics.md |
| People and segments | people.md |
| Error tracking | errors.md |
| Metrics, uptime, alerts, incidents | metrics.md |
| Feature flags | flags.md |
| Revenue | revenue.md |
| Ad destinations | destinations.md |
| Campaigns | communications.md |
| The AI assistant | assistant.md |
| Documentation | docs.md |
Tokens
Two kinds, both Authorization: Bearer:
| Kind | Format | Acts as | Reaches |
|---|---|---|---|
| Personal | spm_pat_… |
its owner | every organization that owner is a member of, with that organization's role permissions |
| Organization | spm_org_… |
the organization | exactly the organization it was minted in, with the developer role |
Create one in the console: Organization → Access tokens, or Account → Personal access tokens. The value is shown exactly once; only a sha256 hash, a 12-character prefix and the last four characters are stored.
A personal token stops working the moment its owner loses the membership, and either kind stops the moment it is revoked or its expiry passes.
Scopes
A scope caps a token; it never widens it. The effective permission set is always the intersection of the bearer's role permissions and the token's scopes.
| Scope | Grants |
|---|---|
read |
every .read permission the bearer holds |
write |
read, plus creating and editing module data |
admin |
write, plus ingest keys and members |
Deleting an organization or an application, changing members, and minting further tokens are never available to a token, at any scope: those need a console session with a fresh passkey.
A token can never be more capable than the member who minted it — an operator
who cannot rotate keys cannot mint an admin token that can.
Application references
{app} accepts either form:
/api/v1/apps/1f0c…/events # application id
/api/v1/apps/saaspromax/saaspro-dev/events # orgSlug/appSlug
GET /orgs/{org}/apps returns both. {org} accepts a slug or an id.
Environments
App-scoped reads take ?env=production|staging|development (an environment id
also works). Production is the default. An unknown value is a 400, never a
silent fallback.
Ranges
Analytics and revenue endpoints take ?range=:
24h 7d 30d 90d 12m custom:2026-08-01..2026-08-31
Days are bucketed in the application's timezone, not yours and not the
server's, so a "day" means the same thing here as it does in the console.
Metrics use their own vocabulary: 1h, 6h, 24h, 7d, 30d, 90d.
Pagination
List endpoints answer:
{ "data": [ … ], "nextCursor": "eyJrIjoi…" }
A cursor is opaque — pass it back unchanged as cursor (or before, where the
endpoint says so) and stop when nextCursor is null. Never construct one.
limit is 1–200 and defaults per endpoint.
curl -H "Authorization: Bearer $SPM_TOKEN" \
"https://saaspro.dev/api/v1/apps/acme/site/events?limit=50&cursor=$CURSOR"
Errors
Every failure is the same shape:
{ "error": "This token is missing the keys.read permission", "code": "forbidden" }
| Code | Status | Meaning |
|---|---|---|
bad_request |
400 | The query or body is invalid; error names the field |
unauthorized |
401 | Missing, malformed, unknown, revoked or expired token |
insufficient_scope |
403 | The token's scope is too narrow for this endpoint |
forbidden |
403 | The bearer lacks the permission, or the module is off |
not_found |
404 | No such organization, application or resource for this token |
method_not_allowed |
405 | Wrong method; the Allow header lists the right ones |
conflict |
409 | A slug or key already exists, or the resource is already in that state |
payload_too_large |
413 | Request body over 256 KB |
rate_limited |
429 | Over the per-token budget; see Retry-After |
not_available |
501 | The module is not built into this deployment |
internal_error |
500 | Quote the X-Request-Id when reporting it |
An organization or application the token cannot reach answers 404, not 403:
membership is never leaked through a different status code.
Rate limits
120 requests per minute, per token. Every response carries:
RateLimit-Limit: 120
RateLimit-Remaining: 118
RateLimit-Reset: 47
and a 429 additionally carries Retry-After in seconds. The budget is per
token, so a noisy script does not starve an interactive session.
Request ids
Every response carries X-Request-Id. Send your own to have it echoed back —
useful for stitching a client trace to a platform log.
Auditing
Every write through the API lands in the append-only audit ledger as an action
by the person the token belongs to, with metadata.via = "management-api" and
the token's id. A revoked token's blast radius is therefore always answerable.
What the API deliberately does not do
- Return a secret twice. An ingest key value exists only in the response
that minted it.
GET /apps/{app}/keysreturns prefixes. - Show raw personal data without the permission. Emails and names are masked
unless the bearer holds
people.pii.read. - Delete organizations or applications, change members, or mint tokens.
- Author or send campaigns. A send needs a second operator's approval and a fresh passkey; campaigns are read-only here.