Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Sandbox vs Live

Every ScaiSend send runs in one of two modes: live (actually delivered) or sandbox (validated, recorded, but not delivered). This page describes when each applies, how to force sandbox, and how to verify what happened.

The two mechanisms#

Sandbox mode turns on in two independent ways:

  1. The request uses a test API key (sg_test_*). Every request made with the key is sandbox. There is no way to override this to live.
  2. The request body sets mail_settings.sandbox_mode.enable: true. Per-request override; works on live and test keys alike.

Either mechanism is sufficient. If both are absent, the send goes live.

Key type sandbox_mode.enable Result
sg_live_* absent or false Live send
sg_live_* true Sandbox
sg_test_* absent or false Sandbox
sg_test_* true Sandbox

What happens in each mode#

Live mode#

  • Request is validated (schema, size, attachment count, template_id format, etc.).
  • If From: domain isn't verified, the request fails synchronously.
  • Message is persisted with status QUEUED.
  • Worker service picks it up, renders the template, applies tracking, builds MIME.
  • SMTP service picks it up, resolves MX, DKIM-signs, connects, delivers.
  • Events (processed, delivered, bounce, deferred, open, click) are recorded and fanned out to webhooks.
  • Status progresses: QUEUEDPROCESSINGSENDINGDELIVERED (or BOUNCED / FAILED).

Sandbox mode#

  • Request is validated the same way as live.
  • Domain verification is not checked. You can send from an unverified domain in sandbox.
  • Message is persisted with status SANDBOX.
  • Nothing is queued to the SMTP service. The Worker doesn't render the template; no MIME is built.
  • No events are fanned out. You won't receive delivered, bounce, etc.
  • Response shape is identical to live — same message_id, same HTTP status.

Sandbox messages are visible via GET /v3/messages and GET /v3/messages/{id} with status sandbox.

Forcing sandbox on a live key#

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
curl -X POST https://scaisend.scailabs.ai/v3/mail/send \
  -H "Authorization: Bearer $SCAISEND_LIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "personalizations": [{"to": [{"email": "test@example.com"}]}],
    "from": {"email": "hello@mail.example.com"},
    "subject": "Sandbox check",
    "content": [{"type": "text/plain", "value": "This will not be delivered."}],
    "mail_settings": {
      "sandbox_mode": {"enable": true}
    }
  }'
python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import os, httpx

resp = httpx.post(
    "https://scaisend.scailabs.ai/v3/mail/send",
    headers={"Authorization": f"Bearer {os.environ['SCAISEND_LIVE_KEY']}"},
    json={
        "personalizations": [{"to": [{"email": "test@example.com"}]}],
        "from": {"email": "hello@mail.example.com"},
        "subject": "Sandbox check",
        "content": [{"type": "text/plain", "value": "This will not be delivered."}],
        "mail_settings": {"sandbox_mode": {"enable": True}},
    },
)
print(resp.status_code, resp.json())
typescript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const resp = await fetch("https://scaisend.scailabs.ai/v3/mail/send", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SCAISEND_LIVE_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    personalizations: [{ to: [{ email: "test@example.com" }] }],
    from: { email: "hello@mail.example.com" },
    subject: "Sandbox check",
    content: [{ type: "text/plain", value: "This will not be delivered." }],
    mail_settings: { sandbox_mode: { enable: true } },
  }),
});

Using test keys in CI#

Create a dedicated test key for CI:

bash
1
2
3
4
curl -X POST https://scaisend.scailabs.ai/v3/api_keys \
  -H "Authorization: Bearer $SCAISEND_JWT" \
  -H "Content-Type: application/json" \
  -d '{"name": "ci-tests", "environment": "test", "scopes": ["mail.send"]}'

Store the returned sg_test_* key as a CI secret. Your test suite can call /v3/mail/send against a real ScaiSend instance without sending real mail — request validation, template rendering, and response shape all behave exactly as they would in production.

This is the pattern to prefer over per-request sandbox_mode. Environment isolation via the key type is a stronger guarantee than a body flag that a misconfigured environment variable could flip.

Confirming a send was sandboxed#

bash
1
2
curl https://scaisend.scailabs.ai/v3/messages/msg_01HXYZ \
  -H "Authorization: Bearer $SCAISEND_API_KEY"

The status field is sandbox. The events[] array is empty (or contains only processed if your deployment records that even for sandbox — varies by version).

json
1
2
3
4
5
6
7
8
9
{
  "id": "msg_01HXYZ",
  "status": "sandbox",
  "from_email": "hello@mail.example.com",
  "to_emails": ["test@example.com"],
  "subject": "Sandbox check",
  "created_at": "2026-04-23T10:00:00Z",
  "events": []
}

What sandbox does not do#

  • No rate limits are enforced differently. Sandbox sends count against your tenant's rate limit.
  • No statistics are incremented. GET /v3/stats ignores sandbox messages. You won't see them in processed counts.
  • No webhooks fire. Your production webhook endpoint won't be bothered by CI traffic.
  • Attachments are not stored. ScaiSend accepts them (and validates size and type) but doesn't persist them to S3.

When to use each#

Scenario Mechanism
CI pipeline, end-to-end integration tests Test key (sg_test_*)
Staging environment Test key
Local developer laptops Test key, one per developer
Production smoke test immediately after deploy Live key with sandbox_mode.enable: true
Debugging a send failure in production without delivering Live key with sandbox_mode.enable: true
Production traffic Live key

What's next#

Updated 2026-05-17 01:33:26 View source (.md) rev 1