Sending

Testing & sandbox

Build and verify your full send-and-webhook flow before you send one real email. Then smoke-test production for free.

Sendara gives you two ways to test. The sandbox takes a test key. It simulates a delivered, a bounced and a complained outcome. It sends no real mail, it bills nothing, and it still calls your webhooks. A verified test recipient receives real mail at one of your own addresses, at no charge, so you can check the message on your verified domain before you launch.

Test keys

Every account holds a live key and a test key. A test key (sk_test_…) runs the full pipeline: idempotency, suppression, templates, events, and webhooks. It does not give the message to a real provider. Sendara sends nothing and charges nothing. A live key (sk_live_…) sends real mail, and Sendara charges for it.

To move from the sandbox to production, change a single environment variable from sk_test_… to sk_live_…. The request shape is identical.

Wire your test suite and local dev against a test key. Because webhooks still fire, you can exercise your event handlers end to end long before you publish a sending domain.

Simulated outcomes

In the sandbox, the recipient address drives the simulated outcome. Sendara reads the local-part (the part before the @), so the domain can be anything. Send to bounced@ to drive a bounce, complained@ to drive a complaint, or anything else to get a clean delivery.

Recipient local-partFinal statusEvents that fire
delivered@deliveredsent → delivered
bounced@ / bounce@bouncedsent → bounced
complained@ / complaint@complainedsent → complained
anything elsedeliveredsent → delivered

The example below drives a simulated bounce. Swap the local-part to steer the outcome. The rest of the request stays the same.

sandbox.ts
import { Sendara } from "sendara";

const sendara = new Sendara("sk_test_xxx");

await sendara.emails.send({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Sandbox bounce",
  html: "<p>This drives a simulated bounce.</p>",
});
Every simulated send records sent before its terminal event. A simulated bounced or complainedoutcome does not add the address to your suppression list. This keeps the sandbox addresses reusable. A real permanent SES bounce or complaint does suppress the recipient in production.

Webhooks in the sandbox

Sandbox sends use the same signed webhook envelope and event types as production, so you can test your handler end to end. The nested payload carries a synthetic detail instead of an Amazon SES event. Sendara delivers each event as a POST with these headers:

  • Sendara-Event-Id: unique per event. Sendara reuses it across the retries of the same event, so use it to dedupe.
  • Sendara-Event-Type: sent, followed by the simulated terminal event (delivered, bounced, or complained).
  • Sendara-Timestamp: the Unix epoch seconds at the time Sendara computed the signature.
  • Sendara-Signature: the HMAC signature (see below).
{
  "event_id": "evt_2",
  "event_type": "delivered",
  "message_id": "msg_a1b2c3",
  "account_id": "acc_7Hb3Kp",
  "payload": { "detail": "simulated delivery (sandbox)" },
  "occurred_at": "2026-06-14T10:00:12Z",
  "created_at": "2026-06-14T10:00:12Z"
}

Verify the signature

Verify every webhook before trusting it. The signature is HMAC-SHA256(secret, "{timestamp}.{raw_body}"), hex-encoded. Build the signed string by concatenating the Sendara-Timestamp header, a literal ., and the exact raw request body. Then compare in constant time.

Sign the raw bytes of the body, before any JSON parsing or re-serialization. Re-encoding changes whitespace and key order, which breaks the signature.
verify.ts
import {
  verifyWebhook,
  WebhookVerificationError,
} from "sendara";

export function verifySendaraWebhook(req: {
  rawBody: string;
  headers: Record<string, unknown>;
}): boolean {
  try {
    verifyWebhook(
      req.rawBody,
      req.headers,
      process.env.SENDARA_WEBHOOK_SECRET!,
      { toleranceSeconds: 5 * 60 },
    );
    return true;
  } catch (error) {
    if (error instanceof WebhookVerificationError) return false;
    throw error;
  }
}

Treat handlers as idempotent. Sendara can deliver an event more than once, and a retry reuses the same Sendara-Event-Id. Sendara retries a failed delivery with a backoff. Respond 2xx to acknowledge.

Verified test recipients

The sandbox simulates an outcome, and it puts no real mail in a real inbox. Register a verified test recipient to send real mail from your verified domain to an address that you own. Open the message there to check how it renders in that mail client, and to confirm that your DKIM signature and your sender name are correct. The send costs nothing, and it never touches your live list.

The guardrails:

  • Up to 3 test recipients per account.
  • Each verified address accepts at most 10 test sends per day (per UTC day).
  • A send uses a live key with test_send: true, and Sendara never bills it.

Register & verify

Register an address with POST /v1/test-recipients (requires the admin scope). Sendara emails that address a verification link. Once the recipient clicks it, the address flips to verified and is eligible for test sends.

curl https://api.sendara.dev/v1/test-recipients \
  -H "Authorization: Bearer sk_live_admin_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "email": "[email protected]" }'
{
  "id": "tr_4f1a8c2d9e",
  "email": "[email protected]",
  "status": "pending",
  "created_at": "2026-06-14T10:00:00Z"
}
emailstringRequired
One of your own addresses to validate against. Each account can have up to 3, and Sendara sends a verification email on creation.

List your recipients with GET /v1/test-recipients (requires read) to check verification status. You can re-send the verification email with POST /v1/test-recipients/{id}/resend, or remove an address with DELETE /v1/test-recipients/{id} (both require admin).

{
  "recipients": [
    { "id": "tr_4f1a8c2d9e", "email": "[email protected]",
      "status": "verified", "verified_at": "2026-06-14T10:02:30Z",
      "created_at": "2026-06-14T10:00:00Z" }
  ]
}

Send a real test email

After the address reaches verified, send to it with a normal POST /v1/send using a live key and test_send: true. The destination must be a verified test recipient on your account, and the per-day cap applies. Sendara skips the spend cap, because the send is free.

test-send.ts
await sendara.emails.send({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Production smoke test",
  html: "<p>Real email, no charge.</p>",
  testSend: true,
});
Use this flow for a production smoke test, for a UAT sign-off, and to confirm that a newly verified domain sends real mail. Do all of it before you point your application at a real customer. No provider can promise inbox placement, because placement depends on your content and your recipients. A test send shows you the rendered message, and it does not report which folder holds it.

Errors

Two error codes are specific to this flow:

  • recipient_not_verified (403): the destination is not a verified test recipient on your account. Register and verify it first, or drop test_send to send normally.
  • test_send_daily_limit (429): this address used its 10 free test sends for the current UTC day. Wait for the next day or use a different verified address.

A request to register a fourth address returns too_many_test_recipients (422). Delete one first.

From sandbox to production

A reliable path to your first real production send:

  • Build against sk_test_… and drive delivered@, bounced@, and complained@ to exercise every branch and your webhook handler.
  • Verify your sending domain (see Domains & deliverability).
  • Register a verified test recipient. Send a real message with test_send: true to check how your verified domain renders the message. The send costs nothing.
  • Swap to sk_live_… and ship.