BETA
Skip to content

Emails

Send, retrieve, and cancel transactional and broadcast email through the Krafter Mail API.

Base URL: https://app.krafter.dev/api/v1

Send Email

Sends a single email to one or more recipients.

POST /emails

Required scope: mail:send

Request Body

FieldTypeRequiredDescription
fromstringYesSender address. Must belong to a verified domain owned by your team.
tostring or arrayYesRecipient(s). A single address or an array of addresses.
subjectstringYesEmail subject line.
htmlstringone-ofHTML body. At least one of html or text is required.
textstringone-ofPlain-text body. At least one of html or text is required.
ccarray of stringsNoCC recipients. Defaults to [].
bccarray of stringsNoBCC recipients. Defaults to [].
reply_tostringNoReply-to address.
tagsobjectNoKey-value metadata, stored as a map. Defaults to {}.
attachmentsarray of objectsNoFile attachments. Each entry: { "filename": "...", "content_type": "...", "content": "<base64>" }. Defaults to [].
streamstringconditional"transactional" or "broadcast". See Stream selection below.
tracking_enabledbooleanNoPer-message override for open/click tracking. See Per-message tracking.
headersobjectNoCustom outbound MIME headers, as a {string: string} object. See Custom headers.
scheduled_atstring (ISO 8601)NoFuture timestamp for delayed delivery (UTC, ISO 8601). No hard upper bound is enforced today; sending more than ~30 days out is not recommended.
template_idstringNoUUID of a saved template. When provided, the template's subject, html, and text replace the request fields after substitution.
variablesobjectNoValues used to replace placeholders when template_id is set. Missing variables are replaced with empty string.

In addition, the Idempotency-Key request header (not a body field) prevents duplicate sends within a 24-hour window. See Idempotency.

Stream selection

Every send is routed through one of two streams:

  • transactionaltracking_enabled: false by default, no List-Unsubscribe header.
  • broadcasttracking_enabled: true by default, List-Unsubscribe header added.

Behaviour of the stream request field, given the API key's allowed_streams:

Key has allowed_streamsstream fieldResult
Exactly one entry, e.g. ["transactional"]omittedDefaults to that single allowed stream
Exactly one entryprovidedMust match; otherwise 403 stream_not_allowed
Both streamsomitted422 stream_required
Both streams"transactional" or "broadcast"Used as given
Any value not transactional/broadcast(anything)422 invalid_stream

Per-message tracking

The tracking_enabled boolean overrides the resolved stream's default for a single message. It must be a plain boolean (true or false); strings are ignored.

  • On the transactional stream the default is false; pass tracking_enabled: true to opt this single send into open/click tracking.
  • On the broadcast stream the default is true; pass tracking_enabled: false to opt out.

Tracking is applied through the SES configuration set attached to the resolved policy — Krafter does not insert HTML pixels or rewrite links in the body of your message. Sandbox keys ignore the override (sandbox always uses a fixed policy).

Custom headers

Pass a headers object to add custom MIME headers to the outgoing message. Keys and values must both be strings; headers are forwarded verbatim through SES.

json
{
  "headers": {
    "X-Custom-Tracking-Id": "abc-123",
    "List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
  }
}

The following header names are reserved and rejected with 422 forbidden_header:

From, To, Cc, Bcc, Subject, Date, Message-ID, Content-Type,
Content-Transfer-Encoding, MIME-Version, DKIM-Signature, Authorization

Any header name (case-insensitively) starting with X-Krafter- is also reserved. A non-object headers value (e.g. a string or array) returns 422 forbidden_header.

Idempotency

Include an Idempotency-Key header to prevent duplicate sends within a 24-hour window:

Idempotency-Key: order-confirmation-456

If the same key is replayed within 24 hours, the original email record is returned and no duplicate send is created. Keys must not exceed 255 bytes.

Example Request

bash
curl -X POST https://app.krafter.dev/api/v1/emails \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: invoice-1042" \
  -d '{
    "from": "billing@yourdomain.com",
    "to": ["alice@example.com"],
    "cc": ["bob@example.com"],
    "subject": "Your invoice is ready",
    "html": "<h1>Invoice #1042</h1><p>Your invoice for January 2026 is ready.</p>",
    "text": "Invoice #1042\n\nYour invoice for January 2026 is ready.",
    "reply_to": "billing@yourdomain.com",
    "tags": {"category": "invoice", "month": "2026-01"},
    "stream": "transactional",
    "headers": {
      "X-Entity-Ref-ID": "inv-1042"
    }
  }'

Example Response

json
// 201 Created
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "from": "billing@yourdomain.com",
    "to": ["alice@example.com"],
    "cc": ["bob@example.com"],
    "bcc": [],
    "reply_to": "billing@yourdomain.com",
    "subject": "Your invoice is ready",
    "status": "queued",
    "tags": {"category": "invoice", "month": "2026-01"},
    "scheduled_at": null,
    "sent_at": null,
    "delivered_at": null,
    "opened_at": null,
    "clicked_at": null,
    "bounced_at": null,
    "complained_at": null,
    "error_reason": null,
    "created_at": "2026-01-15T10:30:00Z"
  }
}

Error responses

Policy and validation problems return one of the standard Mail error shapes documented in Error Handling. The most common cases for POST /emails:

json
// 422 — forbidden header
{
  "error": "header 'Content-Type' is reserved and cannot be set",
  "code": "forbidden_header"
}
json
// 422 — stream required
{
  "error": "stream is required for this API key",
  "code": "stream_required"
}
json
// 403 — stream not allowed
{
  "error": "API key is not allowed to send on stream 'broadcast'",
  "code": "stream_not_allowed"
}

The full code enum (including sandbox_*, recipient_not_allowed, override_forbidden, etc.) is listed in Error Handling.


Send Batch

Sends multiple distinct emails in a single request. Each item in the emails array becomes its own email record.

POST /emails/batch

Required scope: mail:send

Request Body

FieldTypeRequiredDescription
emailsarray of objectsYesUp to 100 email objects. Each object accepts the same fields as Send Email.

Each item in emails is validated independently. If any item fails policy or validation, the entire batch is rejected and a single error is returned (with the offending index in the message). On success the response status is 201 Created; if some items succeeded after policy resolution but failed at insert time, the response is 207 Multi-Status with per-item results.

Example Request

bash
curl -X POST https://app.krafter.dev/api/v1/emails/batch \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "emails": [
      {
        "from": "hello@yourdomain.com",
        "to": "alice@example.com",
        "subject": "Welcome Alice",
        "html": "<p>Welcome!</p>",
        "stream": "transactional"
      },
      {
        "from": "hello@yourdomain.com",
        "to": "bob@example.com",
        "subject": "Welcome Bob",
        "html": "<p>Welcome!</p>",
        "stream": "transactional"
      }
    ]
  }'

Example Response

json
// 201 Created
{
  "data": [
    {
      "status": 201,
      "data": {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "from": "hello@yourdomain.com",
        "to": ["alice@example.com"],
        "cc": [],
        "bcc": [],
        "reply_to": null,
        "subject": "Welcome Alice",
        "status": "queued",
        "tags": {},
        "scheduled_at": null,
        "sent_at": null,
        "delivered_at": null,
        "opened_at": null,
        "clicked_at": null,
        "bounced_at": null,
        "complained_at": null,
        "error_reason": null,
        "created_at": "2026-01-15T10:30:00Z"
      }
    }
  ]
}

When some items in the batch fail at insert time, the wrapper status is 207 Multi-Status and the failing entries appear as {"status": 422, "errors": {...}} alongside the successful ones.


List Emails

Returns a paginated list of emails for the authenticated team.

GET /emails

Required scope: mail:read

Query Parameters

ParameterTypeDefaultDescription
pageinteger1Page number.
per_pageinteger20Results per page. Capped at 100.
statusstring--Filter by status: queued, sent, delivered, bounced, complained, failed, cancelled.
searchstring--Free-text filter on from, to, or subject.

Example Request

bash
curl "https://app.krafter.dev/api/v1/emails?status=delivered&per_page=10" \
  -H "Authorization: Bearer kr_live_abc123def456"

Example Response

json
{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "from": "billing@yourdomain.com",
      "to": ["alice@example.com"],
      "cc": [],
      "bcc": [],
      "reply_to": null,
      "subject": "Your invoice is ready",
      "status": "delivered",
      "tags": {"category": "invoice"},
      "scheduled_at": null,
      "sent_at": "2026-01-15T10:30:01Z",
      "delivered_at": "2026-01-15T10:30:02Z",
      "opened_at": null,
      "clicked_at": null,
      "bounced_at": null,
      "complained_at": null,
      "error_reason": null,
      "created_at": "2026-01-15T10:30:00Z"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 10,
    "total": 156
  }
}

Get Email

Returns details for a single email.

GET /emails/:id

Required scope: mail:read

Example Request

bash
curl https://app.krafter.dev/api/v1/emails/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer kr_live_abc123def456"

Example Response

The response is a single email object using the same shape returned by Send Email:

json
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "from": "billing@yourdomain.com",
    "to": ["alice@example.com"],
    "cc": [],
    "bcc": [],
    "reply_to": null,
    "subject": "Your invoice is ready",
    "status": "delivered",
    "tags": {"category": "invoice"},
    "scheduled_at": null,
    "sent_at": "2026-01-15T10:30:01Z",
    "delivered_at": "2026-01-15T10:30:02Z",
    "opened_at": "2026-01-15T10:31:15Z",
    "clicked_at": null,
    "bounced_at": null,
    "complained_at": null,
    "error_reason": null,
    "created_at": "2026-01-15T10:30:00Z"
  }
}

Cancel Email

Cancels a queued or scheduled email that has not yet been sent.

DELETE /emails/:id

Required scope: mail:send

Example Request

bash
curl -X DELETE https://app.krafter.dev/api/v1/emails/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer kr_live_abc123def456"

Success Response

json
// 200 OK
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "from": "billing@yourdomain.com",
    "to": ["alice@example.com"],
    "subject": "Your invoice is ready",
    "status": "cancelled",
    "...": "(remaining fields as in the full email object)"
  }
}

If the email has already left the queue, the response is 422 Unprocessable Entity:

json
{
  "error": "Email has already been processed and cannot be cancelled"
}

Get Email Events

Returns the event timeline for a specific email — every state change, open, click, bounce, and complaint that has been recorded.

GET /emails/:id/events

Required scope: mail:read

Example Request

bash
curl https://app.krafter.dev/api/v1/emails/550e8400-e29b-41d4-a716-446655440000/events \
  -H "Authorization: Bearer kr_live_abc123def456"

Example Response

json
{
  "data": [
    {
      "id": "660e8400-e29b-41d4-a716-446655440000",
      "type": "queued",
      "data": {},
      "occurred_at": "2026-01-15T10:30:00Z",
      "created_at": "2026-01-15T10:30:00Z"
    },
    {
      "id": "660e8400-e29b-41d4-a716-446655440001",
      "type": "delivered",
      "data": {
        "remote_mta": "mx.example.com",
        "diagnostic_code": "250 2.0.0 OK"
      },
      "occurred_at": "2026-01-15T10:30:02Z",
      "created_at": "2026-01-15T10:30:02Z"
    },
    {
      "id": "660e8400-e29b-41d4-a716-446655440002",
      "type": "opened",
      "data": {
        "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
        "ip": "203.0.113.42"
      },
      "occurred_at": "2026-01-15T10:31:15Z",
      "created_at": "2026-01-15T10:31:15Z"
    }
  ]
}

The shape of the per-event data object varies by event type — typical metadata is shown above. The type field uses the same vocabulary as the email statuses (queued, sent, delivered, bounced, complained, opened, clicked, failed).


Email Statuses

An email progresses through these statuses:

queued -> sent -> delivered
                 -> bounced
                 -> complained
       -> failed
       -> cancelled
StatusDescription
queuedAccepted and waiting to be sent.
sentHanded off to the upstream provider (Amazon SES).
deliveredConfirmed delivery to the recipient's mail server.
bouncedHard or soft bounce reported by the recipient mail server.
complainedRecipient marked the email as spam.
failedSend failed (post-retry) due to a non-retryable error.
cancelledCancelled before being sent.

Built by Krafter Studio