---
title: Philosophy
path: concepts/philosophy
status: published
---

# Philosophy

A few design principles shape the ScaiSend API. Knowing them helps you predict how an unfamiliar endpoint will behave.

## Compatibility over novelty

The `/v3/` surface matches SendGrid's v3 API on purpose. Same field names, same shapes, same HTTP codes, same 202-on-accept for `/mail/send`. When SendGrid's contract has a quirk — like `from` being a reserved Python keyword and the response sometimes returning `message_id` vs `message_ids` — ScaiSend keeps the quirk. You don't want a "cleaner" version of the SendGrid API; you want the SendGrid API, backed by infrastructure you control.

New capabilities that don't exist in SendGrid (image library, sender domain verification, admin endpoints) live under different path prefixes (`/v3/images`, `/api/admin/`) so they're clearly non-compatible.

## Multi-tenant by default

There is no single-tenant mode. Every request is resolved to a tenant — via the API key it was signed with, or via the JWT's `tenant_id` claim — before any business logic runs. Templates, suppressions, stats, messages, API keys, and webhook endpoints are all tenant-scoped. Cross-tenant data leakage isn't a bug class we're worried about; the database queries don't return rows from other tenants because the query predicate requires the active tenant.

If you're running a platform with customers, you don't need a second layer. Create a Tenant per customer, and the isolation is already there.

## Async delivery, sync validation

`POST /v3/mail/send` returns `202 Accepted` after validating the request and queuing it to Redis. Actual SMTP delivery happens out-of-band, in the worker and SMTP services. Two consequences:

- **What you can learn at send time:** whether your request is malformed, whether you have permission, whether the template renders, whether you've exceeded rate limits. These return `4xx` synchronously.
- **What you can't learn at send time:** whether the recipient's MX server accepts the message, whether it bounces, whether it gets marked as spam. These are observable only via webhooks or the message event timeline.

Design your integration assuming `202` means "accepted for processing," not "delivered." Use webhooks or the `GET /v3/messages/{id}` endpoint to confirm delivery.

## Sandbox is an API key property

SendGrid's sandbox mode is a per-request flag (`mail_settings.sandbox_mode.enable: true`). ScaiSend supports that flag but adds a stronger guarantee: **test API keys** (format `sg_test_*`) always force sandbox mode, regardless of the request body. This means your CI pipeline can hold a test key and you can't accidentally send real email even if a misconfigured env var leaks the flag.

Both behaviors work. Pick whichever fits your deployment — per-request for ad-hoc, per-key for environment isolation.

## Opinionated defaults, explicit overrides

ScaiSend ships with defaults that match what SendGrid customers expect:

- Open tracking and click tracking are **on** by default. Disable per-request (`tracking_settings.open_tracking.enable: false`) or per-tenant (`PUT /api/admin/tenants/{id}/tracking`).
- Subscription tracking is **off** by default; enable it when you need an unsubscribe footer.
- Image embedding uses `proxy` mode by default — images are served via `/i/{image_id}` so you get a backup open signal when the tracking pixel is blocked. Switch to `cid` if you'd rather embed images as attachments (bigger messages, no image-load tracking).

The tenant-level setting is the default; request-level settings override on a single send. Both layers are explicit; neither is inferred from your account tier.

## Machine-readable where it matters

Error responses follow either FastAPI's `{"detail": "..."}` shape (for routing and auth errors) or SendGrid's `{"errors": [{"message": ..., "field": ...}]}` shape (for `/v3/mail/send` validation). The HTTP status code classifies the error; the body gives details. Field-level validation errors identify the offending field so you can surface a precise message in your UI.

Events fanned out through webhooks are stable: `processed`, `delivered`, `bounce`, `open`, `click`, `spam_report`, `unsubscribe`, `group_unsubscribe`, `group_resubscribe`, `deferred`, `dropped`. Branch on the event type string, not on fields of the payload.

## Self-hosting as the only mode

There's no ScaiSend Cloud. Every instance is customer-operated, and every feature — including registration with ScaiKey, DKIM key generation, and bounce-inbound handling — has to work without phoning home. The CLI (`scaisend register`, `scaisend sync --full`) exists specifically so a fresh install can bootstrap itself against a ScaiKey instance and be sending within minutes.

## What's next

- [Architecture](architecture) — what the API, Worker, and SMTP services actually do.
- [Quickstart](../quickstart) — make the theory concrete.
