Viesti Sign in

Transactional email API reference

Send a message with one HTTPS request: POST JSON to https://viesti.email/v1/send with an API key as a Bearer token. The API accepts the message for delivery and returns its ID; delivery happens afterwards and shows in Activity.

Endpoint and authentication

  • POST https://viesti.email/v1/send, over HTTPS only. Any method other than POST gets 405.
  • Authorization: Bearer <API key>, using the same keys as SMTP, created on the Credentials page.
  • Content-Type: application/json; a charset parameter is allowed.

Request body

One JSON object. Fields not listed here are rejected with 400.

Field Type Required Rules
from string yes An address at a domain verified in your workspace, optionally with a display name: "Acme <[email protected]>".
to array of strings yes 1 to 50 addresses, each optionally with a display name. Addresses that differ only in case are sent once.
subject string no UTF-8 without line breaks. Viesti encodes it for the header.
text string one of text, html, unless raw Plain-text body.
html string one of text, html, unless raw HTML body. With text as well, the message is multipart/alternative.
raw string no A complete RFC 5322 message with CRLF line endings, sent as written instead of subject, text and html.

With subject, text and html, Viesti builds the message: From, To with every address in to, Subject, Date, Message-ID and a UTF-8 quoted-printable body. Every recipient sees the others in To; to send individually, make one request per recipient.

For headers the built message does not have, such as Reply-To, Cc, custom headers or attachments, send raw. Then to is the delivery list: every address in it receives the message, whether or not it appears in the headers. The From header must use the same domain as from, and the SMTP header rules apply, including the added Date and Message-ID when they are missing.

The message, built or raw, may be up to 10 MB. The JSON body may be up to six times that plus 64 KiB, to leave room for escaping.

Response

HTTP/1.1 202 Accepted
Content-Type: application/json

{"id":"b695f8a9-369b-4bff-9aff-92ba07a414ca","status":"accepted"}

accepted means Viesti stored and queued the message; it is not yet delivered. The id is the Message ID in Activity, where the delivery timeline and each recipient's result appear. A message the content screen holds for review is also accepted, and Activity shows it as quarantined.

Errors

Errors return a JSON body with an error field, {"error":"…"}, and the message is not sent.

Status error Cause
400 invalid JSON Malformed JSON, a field of the wrong type (to must be an array), or an unknown field.
400 one JSON object required More data follows the object.
400 invalid subject subject contains a line break or NUL.
400 raw cannot be combined with subject/text/html Both forms were sent.
400 too many recipients to has more than 50 addresses, or is empty in a request without raw.
400 invalid message An address does not parse, neither text nor html is set, raw comes with an empty to, or raw breaks the header rules (for example bare LF line endings).
400 Date header is unparseable or too far from the current time The Date header in raw is malformed or more than 24 hours off.
401 Bearer API key required The Authorization header is missing or not Bearer <key>.
401 invalid credentials The key is unknown or revoked.
403 sending not permitted The from domain is not verified in this workspace, the From or Sender header in raw uses another domain, or the workspace is not active.
413 message too large or request too large The message is over 10 MB, or the JSON body is over its limit.
415 application/json required Wrong or missing Content-Type.
422 recipient suppressed At least one address in to is on your suppression list, so nothing was sent. Remove it and resend; see bounces.
422 recipient domain cannot receive mail DNS says the domain of at least one address in to does not exist, has no mail server, or refuses all mail (null MX), usually a typo such as icloud.con. Nothing was sent. Fix the address and resend.
422 rejected by content audit The content screen refused the message. The body also carries its id; it is recorded in Activity and not counted.
429 submission quota exceeded The plan's allowance is used up. Sent with Retry-After: 60; see limits.
429 too many failed authentications; retry later Your address failed to authenticate 10 times within 10 minutes and is blocked for 10 minutes.
503 temporarily unavailable Viesti could not accept the message right now. Sent with Retry-After: 30.

Retries and duplicates

Retry 429 submission quota exceeded and 503 after the Retry-After seconds; other errors fail again until you change the request. A 429 without a JSON body comes from the request rate limit in front of the API; see limits. The API has no idempotency key: if a request times out without a response, the message may already be accepted. When a duplicate would matter, look for it in Activity before you resend.

Examples

Each example reads the key from VIESTI_API_KEY. Replace [email protected] with an address at your verified domain.

curl

curl https://viesti.email/v1/send \
  -H "Authorization: Bearer $VIESTI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <[email protected]>",
    "to": ["[email protected]"],
    "subject": "Your receipt",
    "text": "Thanks for your order.",
    "html": "<p>Thanks for your order.</p>"
  }'

Node.js

With the built-in fetch, available without a flag since Node.js 18 (Node.js documentation; checked 26 September 2026), in an ES module such as send.mjs:

const res = await fetch("https://viesti.email/v1/send", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.VIESTI_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "Acme <[email protected]>",
    to: ["[email protected]"],
    subject: "Your receipt",
    text: "Thanks for your order.",
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(`Viesti ${res.status}: ${body.error}`);
console.log(body.id);

Python

With requests:

import os

import requests

response = requests.post(
    "https://viesti.email/v1/send",
    headers={"Authorization": f"Bearer {os.environ['VIESTI_API_KEY']}"},
    json={
        "from": "Acme <[email protected]>",
        "to": ["[email protected]"],
        "subject": "Your receipt",
        "text": "Thanks for your order.",
    },
    timeout=30,
)
if response.status_code != 202:
    raise RuntimeError(f"Viesti {response.status_code}: {response.json()['error']}")
print(response.json()["id"])

A raw message

The same send with a Reply-To header. JSON strings spell CRLF as \r\n:

curl https://viesti.email/v1/send \
  -H "Authorization: Bearer $VIESTI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "[email protected]",
    "to": ["[email protected]"],
    "raw": "From: Acme <[email protected]>\r\nTo: [email protected]\r\nReply-To: [email protected]\r\nSubject: Your receipt\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nThanks for your order.\r\n"
  }'

The HTTP API has one endpoint. There are no webhooks and no endpoint for message status or suppressions; both are in the dashboard. To send from an existing SMTP mailer instead, see SMTP settings.