Authentication
ScaiSend accepts two kinds of credentials: API keys (for servers and applications) and JWTs (for humans in the admin UI). Both go in the Authorization: Bearer <token> header. The server works out which one it is and validates accordingly.
API keys#
Use for any server-to-server integration — production sends, cron jobs, background workers. API keys are scoped to a tenant, have a configurable permission set, and don't expire unless you set an expires_at.
Format#
1 2 | |
The full secret is shown exactly once — when you create the key. After that, only the first 12 characters (the "prefix") are retrievable. Rotate if you lose it.
Creating a key#
1 2 3 4 5 6 7 8 | |
1 2 3 4 5 6 7 8 9 10 11 12 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Response:
1 2 3 4 5 6 7 8 9 | |
Store the api_key value securely — a secrets manager, a Kubernetes secret, a .env outside version control. It gives the bearer full mail.send permission on your tenant.
Using the key#
1 2 | |
The same header format works for every /v3/ endpoint.
Rotating and revoking#
1 2 3 4 5 6 7 | |
Rotations and revocations take effect immediately. Any in-flight requests using the old key complete; subsequent requests with the old key return 401.
JWTs#
Use for anything that's driven by a human — the admin UI, curl calls during development, scripts run by operators. JWTs carry the user's identity and their permissions; they expire (typically after an hour) and are refreshed out of band.
Getting a JWT#
JWTs come from ScaiKey via OAuth. The admin UI handles the flow; for programmatic access, the /v3/auth/* endpoints expose it:
1 2 3 4 5 6 7 8 9 10 11 | |
Response:
1 2 3 4 5 6 7 8 9 10 | |
Refreshing#
1 2 3 | |
Refresh before access_token expires. ScaiSend validates JWTs against SCAIKEY_JWKS_URL on every request — there's no local cache that will hold stale tokens for you.
Current user info#
1 2 | |
Returns the user, their permissions, their tenant, and their role.
API key vs JWT — when to use which#
| Use case | Credential |
|---|---|
| Production email sends from your app | API key (sg_live_*) |
| CI/CD integration tests | API key (sg_test_*) |
| Cron jobs that call the API | API key |
| Admin UI (web) | JWT |
Your developer running curl at a terminal |
JWT |
| A user managing their own API keys | JWT |
| A user managing their own webhook endpoints | JWT |
| Anything creating API keys | JWT (API keys can't create other keys) |
Never put a JWT in an always-on server process. They expire hourly; you don't want a refresh loop in your send code. Use an API key.
Never put an API key in browser-delivered JavaScript. It leaks to anyone who opens DevTools. If you need a browser to call ScaiSend, proxy the call through your backend.
Permission scopes#
Every API key and every user role carries a set of permission scopes. Each endpoint declares the scope it requires. If your credential doesn't have it, you get 403 Forbidden.
The full list:
| Scope | What it allows |
|---|---|
mail.send |
POST /v3/mail/send |
mail.schedule |
send_at in the past or future; batch operations |
mail.cancel |
Cancel a queued or processing message |
templates.read |
GET /v3/templates* |
templates.write |
POST / PATCH on templates and versions |
templates.delete |
Delete templates or versions |
suppressions.read |
List bounces, spam reports, unsubscribes, groups |
suppressions.write |
Add/remove suppressions, create/edit groups |
stats.read |
/v3/stats* |
stats.export |
Export stats data |
webhooks.read |
List webhook endpoints and event settings |
webhooks.write |
Create/update/delete webhook endpoints |
domains.read |
List sender domains |
domains.write |
Add domains, verify DNS, rotate DKIM |
admin.api_keys |
Create, update, revoke API keys |
admin.users |
Manage user-to-role assignments |
admin.settings |
Edit tenant settings (tracking, defaults) |
Keep API keys minimally scoped. A production send key only needs mail.send; a reporting job only needs stats.read. See Roles and Permissions for the role-based view.
Test-key sandbox#
Any request made with a test key (sg_test_*) is automatically in sandbox mode — the request is fully validated, the template is rendered, the message is written to the database with status sandbox, but nothing is handed to the SMTP service. The response looks identical to a live send.
You can also force sandbox on a live key per-request:
1 2 3 4 5 | |
Both mechanisms exist deliberately. Use test keys in environments that should never send real mail (staging, CI). Use sandbox_mode when testing an integration against a live key.
See Sandbox vs Live for the full semantics.
Common failure modes#
| Response | Likely cause |
|---|---|
401 Unauthorized with {"detail": "Missing Authorization header"} |
No Authorization header. |
401 Unauthorized with {"detail": "Invalid API key"} |
Key was revoked, deleted, or typo'd. Check with GET /v3/api_keys. |
401 Unauthorized with {"detail": "JWT expired"} |
Refresh your token. |
403 Forbidden with {"detail": "Missing required scope: mail.send"} |
Add the scope to the key or use a different key. |
403 Forbidden with tenant-mismatch detail |
You're calling an endpoint for a tenant your credential doesn't belong to. |
What's next#
- Your First Integration — a real send loop with retries and event handling.
- Roles and Permissions — the full RBAC model.
- API Keys Reference — complete endpoint list.