Appearance
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 /emailsRequired scope: mail:send
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Sender address. Must belong to a verified domain owned by your team. |
to | string or array | Yes | Recipient(s). A single address or an array of addresses. |
subject | string | Yes | Email subject line. |
html | string | one-of | HTML body. At least one of html or text is required. |
text | string | one-of | Plain-text body. At least one of html or text is required. |
cc | array of strings | No | CC recipients. Defaults to []. |
bcc | array of strings | No | BCC recipients. Defaults to []. |
reply_to | string | No | Reply-to address. |
tags | object | No | Key-value metadata, stored as a map. Defaults to {}. |
attachments | array of objects | No | File attachments. Each entry: { "filename": "...", "content_type": "...", "content": "<base64>" }. Defaults to []. |
stream | string | conditional | "transactional" or "broadcast". See Stream selection below. |
tracking_enabled | boolean | No | Per-message override for open/click tracking. See Per-message tracking. |
headers | object | No | Custom outbound MIME headers, as a {string: string} object. See Custom headers. |
scheduled_at | string (ISO 8601) | No | Future timestamp for delayed delivery (UTC, ISO 8601). No hard upper bound is enforced today; sending more than ~30 days out is not recommended. |
template_id | string | No | UUID of a saved template. When provided, the template's subject, html, and text replace the request fields after substitution. |
variables | object | No | Values 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:
transactional—tracking_enabled: falseby default, noList-Unsubscribeheader.broadcast—tracking_enabled: trueby default,List-Unsubscribeheader added.
Behaviour of the stream request field, given the API key's allowed_streams:
Key has allowed_streams | stream field | Result |
|---|---|---|
Exactly one entry, e.g. ["transactional"] | omitted | Defaults to that single allowed stream |
| Exactly one entry | provided | Must match; otherwise 403 stream_not_allowed |
| Both streams | omitted | 422 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
transactionalstream the default isfalse; passtracking_enabled: trueto opt this single send into open/click tracking. - On the
broadcaststream the default istrue; passtracking_enabled: falseto 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, AuthorizationAny 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-456If 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/batchRequired scope: mail:send
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
emails | array of objects | Yes | Up 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 /emailsRequired scope: mail:read
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number. |
per_page | integer | 20 | Results per page. Capped at 100. |
status | string | -- | Filter by status: queued, sent, delivered, bounced, complained, failed, cancelled. |
search | string | -- | 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/:idRequired 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/:idRequired 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/eventsRequired 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| Status | Description |
|---|---|
queued | Accepted and waiting to be sent. |
sent | Handed off to the upstream provider (Amazon SES). |
delivered | Confirmed delivery to the recipient's mail server. |
bounced | Hard or soft bounce reported by the recipient mail server. |
complained | Recipient marked the email as spam. |
failed | Send failed (post-retry) due to a non-retryable error. |
cancelled | Cancelled before being sent. |