Quickstart — first email in 5 minutes

Send your first email with Sendoka. Three equivalent paths: curl, fetch (Node), Python.

At the end you'll have:

  • A verified sender domain (or your org's sandbox sender)
  • A live API key
  • A delivered email you can inspect in the dashboard

0. Sign up & grab a key

  1. Create an account at /register. You land in a fresh org.
  2. Go to Overview → API keys → New key.
  3. Pick Test mode for now — keys start with sok_test_… and don't hit real SES. Switch to sok_live_… when you want mail to actually go out — from a verified domain to anyone, or from your sandbox sender to your own team.
  4. Copy the key. It's only shown once.
export SENDOKA_API_KEY="sok_test_..."

Sandbox: every org gets its own {org-slug}.sandbox.sendoka.com sender auto-provisioned, so you can send before verifying a real domain. The hostname is per-org — copy the exact one from Overview → Domains and use it wherever the samples below say {org-slug}. With a live key it only delivers to the verified sign-in addresses of your org's members — every to, cc and bcc must be one, so swap the sample to for your own sign-in address once it is verified (Settings → Your sign-in email shows whether it is, and sends a fresh verification link if not) — at most 100 recipients per org per UTC day, and never an audience broadcast. To mail anyone else, add and verify your own domain.

1. Send an email

curl

curl -sS https://www.sendoka.com/api/v1/emails \
  -H "Authorization: Bearer $SENDOKA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "you@{org-slug}.sandbox.sendoka.com",
    "to": ["you+quickstart@example.com"],
    "subject": "Hello from Sendoka",
    "html": "<p>It works. <strong>Ship it.</strong></p>"
  }'

Response:

{
  "id": "msg_01HN...",
  "channel": "email",
  "status": "sent",
  "created_at": "2026-04-22T10:14:22.113Z"
}

Node (fetch)

const res = await fetch("https://www.sendoka.com/api/v1/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SENDOKA_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "you@{org-slug}.sandbox.sendoka.com",
    to: ["you+quickstart@example.com"],
    subject: "Hello from Sendoka",
    html: "<p>It works. <strong>Ship it.</strong></p>",
  }),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const msg = await res.json();
console.log(msg.id); // msg_...

Python

Use httpx or requests:

import os, httpx

r = httpx.post(
    "https://www.sendoka.com/api/v1/emails",
    headers={"Authorization": f"Bearer {os.environ['SENDOKA_API_KEY']}"},
    json={
        "from": "you@{org-slug}.sandbox.sendoka.com",
        "to": ["you+quickstart@example.com"],
        "subject": "Hello from Sendoka",
        "html": "<p>It works. <strong>Ship it.</strong></p>",
    },
    timeout=30,
)
r.raise_for_status()
print(r.json()["id"])

2. Send an SMS (optional)

Same pattern, different endpoint.

curl -sS https://www.sendoka.com/api/v1/sms \
  -H "Authorization: Bearer $SENDOKA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "+15551234567",
    "to": "+15557654321",
    "body": "Sendoka says hi"
  }'

In test mode, the provider call is simulated — you'll get back status: "sent" with a test_msg_… provider id and nothing hits SNS. Switch to a live key to actually deliver.

3. Watch it happen

  • Overview → Email logs — every email send with its current status. Click into any msg_… for its lifecycle timeline, content, and any provider error.
  • Overview → SMS — the same feed for text messages.
  • X-Request-Id on every response — paste into support tickets.

4. Handle errors

Every error response shares a shape. The code is stable and the request_id is reflected in the X-Request-Id header.

{
  "error": {
    "type": "validation_error",
    "code": "VALIDATION_ERROR",
    "message": "Subject is required",
    "request_id": "req_Hg8JpNvKXWg4f",
    "errors": [
      { "path": ["subject"], "code": "invalid_type", "message": "Required" }
    ]
  }
}

Full catalog: api/errors.md.

5. Next steps

Pick the track that matches where you're going:

Goal Read this next
Send with your own domain features/domains.md
Receive delivery events features/webhooks.md
Templates with variables api/templates.md
Schedule a future send api/scheduled-sends.md
Bulk / audience blast api/audiences.md
Multi-tenant SaaS on top features/platforms.md
Never double-send api/idempotency.md
Understand limits api/rate-limits.md

Troubleshooting

401 UNAUTHORIZED — key is missing, typo'd, revoked, or expired. Regenerate in the dashboard.

403 DOMAIN_NOT_ALLOWED_FOR_KEY — this key is scoped to specific domains and yours isn't on the list. Edit the key or send from an allowed domain.

403 SENDER_DOMAIN_NOT_VERIFIEDfrom isn't on a domain this org has verified. The sandbox hostname is per-org ({org-slug}.sandbox.sendoka.com, not sandbox.sendoka.com) — copy it from Overview → Domains.

403 SANDBOX_RECIPIENT_NOT_ALLOWED — a live send from your sandbox sender went to an address that isn't a verified sign-in address of a member of your org. Send to a member's verified sign-in address, or add and verify your own domain to mail anyone else. If the refused address is a member's and the message says it has not been verified yet, that member verifies it from Settings → Your sign-in email → Send verification link in the dashboard (the session route POST /api/auth/resend-verification); password accounts created before signup required an emailed code often still show as unverified.

429 SANDBOX_DAILY_LIMIT — your sandbox sender's 100 recipients for this UTC day are used up. The error message says when the day rolls over (00:00 UTC); don't retry before then. Your own verified domain has no such cap.

422 VALIDATION_ERROR — inspect error.errors[] for the field that failed. Common: from not a valid email, to empty, neither html nor text nor template present.

429 USAGE_LIMIT_EXCEEDED — free plan hit its monthly cap. Upgrade or wait until the next cycle.

Email "sent" but never arrived — check Overview → Email logs → for the bounce/complaint reason. Spam folder is usually the culprit during warmup.