Notideus
API Reference

Emails

Send a single email or a batch of up to 100.

Base URL: https://api.notideus.io. All endpoints below are under /v1 and use the error envelope on failure — see Errors.

POST /v1/emails

Scope: emails:send — dual-auth: also accepts a session token.

Enqueues one email. Either html or template_id is required; when both are present the template wins.

Request fields

FieldTypeRequiredNotes
fromstringyesAccepted formats: a@b.c, Name <a@b.c>; domain must be verified
tostringyesMin 1 recipient
subjectstringyes
htmlstringrequired unless template_idRaw HTML body
template_iduuidnoDashboard-managed template; wins over html
variablesmapstringstringif the template declares slots{{name}} slots; values HTML-escaped
textstringnoPlain-text alternative; sent as multipart/alternative with the HTML
headersmapstringstringnoCustom message headers (max 10 entries) — see Custom headers
reply_tostringnoAddress replies should go to; display-name forms are reduced to the bare address
tagsstringnoStored as JSON array
idempotency_keystringnoReplay returns 200 + original

Request example

curl -X POST https://api.notideus.io/v1/emails \
  -H "Authorization: Bearer $NT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Notideus <noreply@demo.dev>",
    "to": ["jane@example.com"],
    "subject": "Welcome to Notideus",
    "html": "<p>Hi Jane,</p><p>Your account is ready.</p>",
    "variables": {"name": "Jane"},
    "tags": ["welcome"],
    "idempotency_key": "docs-quickstart-1"
  }'

Custom headers

The headers object attaches custom message headers to the outgoing email — at most 10 entries:

  • Names must be valid header tokens of at most 126 characters.
  • Values must be 1–995 printable-ASCII characters (0x20–0x7e): non-empty, and no control or non-ASCII characters.
  • Reserved names are rejected with 400 invalid_headers: From, To, Cc, Bcc, Subject, Date, Message-ID, MIME-Version, Content-Type, Content-Transfer-Encoding, Return-Path, Received, DKIM-Signature, plus any name starting with X-SES (case-insensitive).

RFC 8058 one-click unsubscribe works on transactional sends:

{
  "headers": {
    "List-Unsubscribe": "<https://example.com/preferences>",
    "List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
  }
}

Response

201 Created — the stored email row:

{
  "id": "01a0a11c-188e-7c9e-9b69-753fcb4c8d0d",
  "team_id": "00000000-0000-0000-0000-000000000001",
  "domain_id": "01a0a118-2c7b-7da6-aa31-ceb20daf80c6",
  "template_id": null,
  "from_address": "Notideus <noreply@demo.dev>",
  "to_emails": ["jane@example.com"],
  "subject": "Welcome to Notideus",
  "html": "<p>Hi Jane,</p><p>Your account is ready.</p>",
  "size_kb": 0.04,
  "tags": ["welcome"],
  "status": "queued",
  "idempotency_key": "docs-quickstart-1",
  "created_at": "2026-09-14T18:09:28.206638Z",
  "requeued_at": null,
  "ses_message_id": null,
  "broadcast_id": null,
  "contact_id": null,
  "unsubscribe_token": null,
  "body_text": "",
  "headers": null,
  "reply_to": ""
}

200 OK — a replay: same request (same idempotency_key) returns the original stored row with 200 instead of 201. See Idempotency.

The row shape:

FieldTypeNotes
iduuidStored email identifier; identical across idempotent replays
team_iduuidOwning team
domain_iduuidMatched verified sending domain
template_iduuid, nullTemplate used, null for raw HTML sends
from_addressstringNormalized From, e.g. Name <a@b.c>
to_emailsstringRecipients exactly as stored
subjectstring
htmlstringRendered HTML after variable substitution
size_kbnumberHTML size in kilobytes
tagsstring
statusstringOne of queued, sent, delivered, opened, clicked, bounced, complained — see Email lifecycle
idempotency_keystring, nullKey the row was stored under, if any
created_attimestampRFC 3339
requeued_attimestamp, nullSet when a send is requeued
ses_message_idstring, nullProvider message id, set once handed to SES
broadcast_iduuid, nullSet for broadcast sends
contact_iduuid, nullSet when addressed to a stored contact
unsubscribe_tokenstring, nullPer-email unsubscribe token
body_textstringPlain-text alternative, empty when unset
headersobject, nullCustom headers exactly as accepted, null when unset
reply_tostringBare reply address, empty when unset

Errors

  • 400 bad_request — malformed JSON or a missing field.
  • 400 invalid_from — the From address cannot be parsed.
  • 400 invalid_headers — a custom header is reserved, malformed or outside the limits above.
  • 400 invalid_reply_to — reply_to cannot be parsed as an address.
  • 402 quota_exceeded — monthly send allowance reached; see Quotas and limits.
  • 403 insufficient_scope — the key lacks emails:send.
  • 403 domain_not_allowed — the key is bound to a different domain than the From address resolves to.
  • 422 from_domain_not_verified — the From domain matches no verified team domain.

All error codes: Errors.

POST /v1/emails/batch

Scope: emails:send — API key only (no session auth).

Enqueues up to 100 emails in one call. The body is an emails array; each item has the same shape as a single send — including the optional text, headers and reply_to fields — with its own idempotency_key.

Request example

{
  "emails": [
    {
      "from": "Notideus <noreply@demo.dev>",
      "to": ["jane@example.com"],
      "subject": "Your weekly digest",
      "html": "<p>Hi Jane, here is your digest.</p>",
      "idempotency_key": "digest-2026-09-14-jane"
    },
    {
      "from": "Notideus <noreply@demo.dev>",
      "to": ["john@example.com"],
      "subject": "Your weekly digest",
      "html": "<p>Hi John, here is your digest.</p>",
      "idempotency_key": "digest-2026-09-14-john"
    }
  ]
}

Response

200 OK — one result per item, in request order. Each item is either {"index","id"} (accepted) or {"index","error"} (rejected):

{
  "data": [
    {
      "index": 0,
      "id": "01a0a11c-4e6e-7228-b9b3-f50fd0728a90"
    },
    {
      "index": 1,
      "id": "01a0a11c-4e75-7486-8f23-8d50dc697ebf"
    },
    {
      "index": 2,
      "error": {
        "code": "from_domain_not_verified",
        "message": "no verified domain matches the from address"
      }
    }
  ]
}

Decode: index is the item's position in the request array (0-based); id is the stored email's UUID, exactly what a single send would return; error carries the same code/message envelope as a whole-call failure. Partial failures never abort the batch — one bad item does not affect the others.

Whole-call failures

  • 400 batch_too_large — more than 100 items; nothing is processed:
{
  "error": {
    "code": "batch_too_large",
    "message": "batch is limited to 100 emails"
  }
}
  • 402 quota_exceeded — the whole batch is checked against your quota up front: if it would not fit, nothing is sent. Idempotent replays of already-stored emails are exempt, so retrying with the same keys never double-counts. See Quotas and limits.

Only the items with an error need retrying — re-send just those with the same per-item idempotency_keys. Full walkthrough: Sending batches.

Copyright © 2026