Skip to content
Sendora Cloud
Create account
Reference

API keys

Every key belongs to one project and carries one environment. Beyond that you choose three things: what it can reach, where it can be used from, and how hard it may be used.

Two kinds

Publishable keys (pk_<env>_…) ship inside your app bundle. They are safe there: they carry a fixed, narrow scope set and are rejected outright on every organisation-admin endpoint. Secret keys (sk_<env>_…) are for your server. Treat one like a password.

Access presets

Secret keys choose an access level at creation. Pick a preset, or open Advanced to select individual scopes.

PresetGrantsReach for it when
Read-onlyEvery PRODUCT read scope: analytics, events, errors, logs, profiles, campaigns, links, configuration.An AI agent, a reporting job, a dashboard you built yourself.
Read & writeEverything above, plus creating and editing product data and sending campaigns.A backend integration that manages content on your behalf.
Full accessEvery scope, including ones added in future releases. Identical to every secret key issued before scopes existed.Your own server, where the key is already trusted with everything.
Neither preset can escalate. Some scopes grant powers that would let a key rewrite its own limits, move money, or destroy data irreversibly. They are excluded from both presets and available only through Advanced: api-keys:read, api-keys:write, members:write, billing:write, gdpr:write, orgs:write, projects:write, providers:write, auth-service:write. The clearest case is api-keys:write: a key that can mint keys can mint itself a full-access one, so a “read & write” preset containing it would be full access wearing a different label.
Presets grant product data, never account data. No preset — not even Read & write — grants your team roster and their roles, your plan and invoices, the organisation audit log, or your end users’ accounts, sessions and linked identities. Those are real scopes and you can grant them under Advanced, but a key you hand to a third party does not get them by default. Read-only means read-only product telemetry.
Presets cover the server surface, not the client one. The scopes your app’s SDK uses on behalf of an end user — signing someone in, registering a device, submitting a survey response — are granted to publishable keys and left out of both presets on purpose: a reporting agent has no business creating accounts. Pick them individually under Advanced if a server-side integration genuinely needs one.
Known gap. A few older endpoints — listing links, feature flags and surveys — still require a legacy combined scope that only Read & write and Full access can grant. A read-only key cannot list those three today. We are splitting those endpoints; until then, use Read & write if you need them.

A scoped key fails closed

If an endpoint has not declared which scope it needs, a key with a restricted scope set cannot call it — it returns 403 rather than being allowed through. This is deliberate and it is the direction that matters: when we add an endpoint and forget to label it, your read-only key stays read-only instead of quietly gaining access to it.

Full-access keys are unaffected, which is every key issued before this shipped. If you hit a 403 on something you believe your key should reach, tell us and we will label the endpoint.

GET /orgs/{orgId}/api-keys/scopes returns the complete catalogue: every scope, what it grants, and exactly what each preset expands to. It needs only me:read, which both presets grant and every publishable key carries — so a key can always ask what it is allowed to do. The response is the same for every organisation; it describes the product, not your data.

IP allowlists

A secret key can be restricted to a set of addresses. Bare addresses or CIDR blocks, IPv4 or IPv6, one per line:

203.0.113.7
198.51.100.0/24
2001:db8::/32

An empty list means no restriction — and it is the only way to express that. We reject 0.0.0.0/0 because it looks like a restriction while allowing everything, which is worse than having none: you would believe the key was locked down.

Requests must arrive through the public API endpoint. We determine your address from the header our edge sets and a caller cannot forge. If that header is absent, a key with an allowlist is refused rather than allowed — an allowlist that admitted requests it could not verify would only hold until someone omitted a header. Use https://api.sendoracloud.com.

Allowlists are editable after the key is issued. Your office IP changing should not force you to rotate a credential and redeploy.

Rate limits

The platform limit is 200 requests per minute per IP address, and that has not changed. On top of it you can give an individual secret key a smaller budget of its own.

A per-key limit is a ceiling you lower, not one you raise. Cap an agent key at 60/min and it can never take more than 60 of the shared allowance, whatever it does — the rest stays available to your production traffic on the same host. That is the isolation worth having, and it is why the limit is per key with no shared organisation or project pool.

By default a key has no limit of its own and is bounded only by the platform limit. Set one when you want a specific integration bounded — an AI agent, a backfill script, anything whose appetite you would rather cap than discover.

Publishable keys cannot carry a per-key budget, deliberately: every install of your app presents the same publishable key, so a shared counter would throttle your entire user base at once. Those requests are bounded per device instead.

Exceeding either limit returns 429 with retryAfterSeconds, and every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Our SDKs map the 429 to a rate_limited error marked retryable, so a well-behaved client backs off without extra work.

Creating a scoped key

curl -X POST https://api.sendoracloud.com/api/v1/orgs/{{ORG_ID}}/api-keys \
  -H "X-API-Key: {{SECRET_KEY}}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "analytics-agent",
    "kind": "secret",
    "projectId": "{{PROJECT_ID}}",
    "scopePreset": "read_only",
    "allowedIps": ["203.0.113.0/24"],
    "rateLimitPerMinute": 60
  }'

The plaintext key is returned once and never again. Pass either scopePreset or an explicit scopes array — not both; we reject the pair rather than guessing which you meant. An unrecognised scope is also rejected, because a key carrying a scope that does not exist would authenticate perfectly and then be refused everywhere, with nothing pointing at the typo.

What cannot change afterwards

Scopes, kind, environment and project assignment are fixed once a key is issued — changing them would silently change what the key can do everywhere it is already deployed. The name, the IP allowlist and the rate limit are all editable, because those describe how a key is operated rather than what it is for. To change a key’s access, mint a new one and revoke the old.