TrivSend developer guide

Everything an application needs to send email through TrivSend, receive it, and react to what happens after. Written for people and for AI coding assistants: every request and response below is the real shape the API returns.

Quick start

Four steps from zero to a delivered email.

  1. Create an account at https://app.trivsend.com/signup and confirm your email address. New accounts are reviewed before they can send to real recipients; you can set everything up, and send with a test key, in the meantime.
  2. Add your sending domain under Domains. Use a subdomain such as notify.example.com, so transactional reputation stays separate from your people's mail. Publish the DNS records it shows, then click Verify. See Domains and DNS.
  3. Create an API key under API keys. Choose Sending access for an app that only sends. The key (ts_...) is shown once; store it as a secret, for example TRIVSEND_API_KEY.
  4. Send:
bash
curl -X POST https://api.trivsend.com/emails \
  -H "Authorization: Bearer $TRIVSEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <noreply@notify.example.com>",
    "to": "user@example.com",
    "subject": "Your sign-in code",
    "html": "<p>Your code is <strong>482913</strong></p>",
    "text": "Your code is 482913"
  }'
json
{ "id": "3f2a1c9e-7b4d-4e8a-9c1f-2d6b8a0e5f71" }

A 200 with an id means TrivSend accepted the email and queued it. Delivery happens in the background; follow it with GET /emails/:id or webhooks.

Authentication

Every request carries an API key as a bearer token:

text
Authorization: Bearer ts_xxxxxxxxxxxxxxxxxxxxxxxx
Key typePrefixCan do
Full accessts_Everything in this guide
Sending accessts_POST /emails, POST /emails/batch, POST /validations, GET /usage. Can be limited to one domain with domain_id
Testts_test_Sending access, but nothing is delivered: every email is accepted, logged and simulated. See Testing

Rules for keys:

Sending email

POST /emails

Send one email.

FieldTypeRequiredNotes
fromstringyes*"Name <addr@domain>" or "addr@domain". The domain must be verified in your account
tostring or string[]yesUp to 50 recipients across to, cc and bcc
cc, bccstring or string[]no
reply_tostring or string[]no
subjectstringyes*Up to 998 characters
htmlstringone of*HTML body
textstringone of*Plain-text body. Send both for the best inbox placement
headersobjectnoExtra headers, e.g. {"X-Entity-Ref-ID": "123"}. From, To, Subject, Message-ID, Date and similar are set by TrivSend
tags{name, value}[]noUp to 50. Letters, numbers, _ and - only. Searchable and included in webhooks
attachmentsobject[]noUp to 20. See Attachments
scheduled_atstringnoSend later. See Scheduling
template{id, variables}noSend a hosted template instead of html/text. See Templates

* With template, from and subject may come from the template instead, and html/text must not be sent.

Optional request header: Idempotency-Key. See Idempotency.

Response 200:

json
{ "id": "3f2a1c9e-7b4d-4e8a-9c1f-2d6b8a0e5f71" }

POST /emails/batch

Send up to 100 emails in one request. The body is a JSON array of POST /emails objects. Either all of them are accepted or none are: a validation error in any one rejects the whole batch.

bash
curl -X POST https://api.trivsend.com/emails/batch \
  -H "Authorization: Bearer $TRIVSEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '[
    {"from": "Acme <noreply@notify.example.com>", "to": "a@example.com", "subject": "Hi A", "text": "Hello"},
    {"from": "Acme <noreply@notify.example.com>", "to": "b@example.com", "subject": "Hi B", "text": "Hello"}
  ]'
json
{ "data": [ { "id": "..." }, { "id": "..." } ] }

Code examples

Node.js (18 or later, no packages needed):

js
const res = await fetch("https://api.trivsend.com/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TRIVSEND_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `welcome-${user.id}`,
  },
  body: JSON.stringify({
    from: "Acme <noreply@notify.example.com>",
    to: user.email,
    subject: "Welcome to Acme",
    html: "<h1>Welcome!</h1>",
    text: "Welcome!",
    tags: [{ name: "category", value: "welcome" }],
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(`TrivSend ${res.status} ${body.name}: ${body.message}`);
console.log("queued", body.id);

Python (requests):

python
import os, requests

res = requests.post(
    "https://api.trivsend.com/emails",
    headers={
        "Authorization": f"Bearer {os.environ['TRIVSEND_API_KEY']}",
        "Idempotency-Key": f"welcome-{user_id}",
    },
    json={
        "from": "Acme <noreply@notify.example.com>",
        "to": user_email,
        "subject": "Welcome to Acme",
        "html": "<h1>Welcome!</h1>",
        "text": "Welcome!",
    },
    timeout=15,
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"TrivSend {res.status_code} {body['name']}: {body['message']}")
print("queued", body["id"])

PHP:

php
$ch = curl_init("https://api.trivsend.com/emails");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("TRIVSEND_API_KEY"),
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "from" => "Acme <noreply@notify.example.com>",
        "to" => $email,
        "subject" => "Welcome to Acme",
        "html" => "<h1>Welcome!</h1>",
    ]),
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status !== 200) throw new Exception("TrivSend $status {$body['name']}: {$body['message']}");

Oracle APEX / PL/SQL (APEX_WEB_SERVICE; the database needs an ACL and a wallet that trusts Let's Encrypt for api.trivsend.com):

sql
declare
  l_body     clob;
  l_response clob;
begin
  select json_object(
           'from'    value 'Acme <noreply@notify.example.com>',
           'to'      value :P1_EMAIL,
           'subject' value 'Your report is ready',
           'html'    value '<p>Hello</p>'
           returning clob)
    into l_body
    from dual;

  apex_web_service.g_request_headers.delete;
  apex_web_service.g_request_headers(1).name  := 'Authorization';
  apex_web_service.g_request_headers(1).value := 'Bearer ' || :TRIVSEND_API_KEY;
  apex_web_service.g_request_headers(2).name  := 'Content-Type';
  apex_web_service.g_request_headers(2).value := 'application/json';

  l_response := apex_web_service.make_rest_request(
    p_url         => 'https://api.trivsend.com/emails',
    p_http_method => 'POST',
    p_body        => l_body);

  if apex_web_service.g_status_code <> 200 then
    raise_application_error(-20001, 'TrivSend: ' || dbms_lob.substr(l_response, 2000, 1));
  end if;
end;

Attachments

json
"attachments": [
  {
    "filename": "invoice-1042.pdf",
    "content": "JVBERi0xLjQKJ...",
    "content_type": "application/pdf"
  },
  {
    "filename": "logo.png",
    "content": "iVBORw0KGgo...",
    "content_id": "logo"
  }
]

Idempotency

Networks fail after the server has acted. To retry safely, send an Idempotency-Key header (up to 256 characters) that is unique per logical email, for example otp-<request id> or invoice-<invoice id>. A repeat request with the same key returns the original email's id and sends nothing new. Keys are scoped to your account.

Scheduling

Set scheduled_at to send later, up to 30 days ahead:

A scheduled email has status scheduled until it goes out. Change or cancel it:

bash
# Reschedule
curl -X PATCH https://api.trivsend.com/emails/$ID \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"scheduled_at": "in 1 hour"}'

# Cancel
curl -X POST https://api.trivsend.com/emails/$ID/cancel -H "Authorization: Bearer $KEY"

Both return { "object": "email", "id": "..." } and need a full-access key. Only emails that are still scheduled can be changed.

Check an email

GET /emails/:id (full access):

json
{
  "object": "email",
  "id": "3f2a1c9e-7b4d-4e8a-9c1f-2d6b8a0e5f71",
  "from": "Acme <noreply@notify.example.com>",
  "to": ["user@example.com"],
  "cc": [],
  "bcc": [],
  "reply_to": [],
  "subject": "Your sign-in code",
  "html": "<p>Your code is <strong>482913</strong></p>",
  "text": "Your code is 482913",
  "tags": [{ "name": "category", "value": "otp" }],
  "status": "sent",
  "last_event": "sent",
  "message_id": "<3f2a1c9e-...@notify.example.com>",
  "template_id": null,
  "test": false,
  "scheduled_at": null,
  "created_at": "2026-10-22T09:14:03.120Z",
  "sent_at": "2026-10-22T09:14:04.501Z"
}
statusMeaning
scheduledWaiting for scheduled_at
queued, sendingAccepted, being delivered
sentThe recipient's mail server accepted it
bouncedThe recipient's server rejected it (after it was sent)
failedCould not be delivered after all retries
suppressedNot sent: every recipient is on your suppression list
canceledA scheduled email you canceled

last_event is the latest thing that happened, which can be newer than the status: delivery_delayed, bounced, complained, opened or clicked.

sent means the receiving server accepted the message. Whether it lands in the inbox or in spam is decided by the receiver; good DNS, consistent sending and clean lists are what move that.

GET /emails (full access) returns the newest first and takes these filters, which combine:

QueryExample
recipientuser@example.com (matches to, cc and bcc)
statussent,bounced
tagcategory=otp
subject, frompartial, case-insensitive
streamtransactional or marketing
testtrue or false
created_after, created_before2026-10-01T00:00:00Z
bash
curl -H "Authorization: Bearer $KEY" \
  "https://api.trivsend.com/emails?recipient=user@example.com&status=bounced&limit=20"

Pagination

Every list endpoint pages the same way:

json
{ "object": "list", "data": [ ... ], "has_more": true }

Keep calling with after set to the last item's id while has_more is true.

Errors

Errors always have this shape:

json
{ "statusCode": 403, "name": "invalid_from_address", "message": "The example.com domain is not verified. Verify its DNS records first." }

Branch on name; show message to developers, not end users.

StatusnameWhat to do
401missing_api_keySend the Authorization: Bearer header
403invalid_api_keyThe key is wrong or revoked
401restricted_api_keyA sending-access key called a full-access endpoint, or used another domain
403invalid_from_addressAdd and verify the from domain
403account_pending_reviewYour account is in review; use a test key until it's approved
403tenant_paused, tenant_suspendedSending is stopped for the account; contact support
404not_foundWrong id, or it belongs to another account
422validation_errorThe message names the field and the problem
429rate_limit_exceededToo many requests per second; retry after a short pause
429daily_quota_exceeded, monthly_quota_exceededPlan limit reached; resets at midnight UTC or on the 1st
500application_errorRetry with the same Idempotency-Key

Retrying

Retry on network errors, 429 rate_limit_exceeded and 5xx, with exponential backoff (for example 1 s, 2 s, 4 s, 8 s) and the same Idempotency-Key, so a retry never sends twice. Don't retry other 4xx errors; fix the request.

Rate limits and quotas

Each account has a requests-per-second limit and daily and monthly email quotas, set by its plan. Quotas count emails that can reach a real mailbox; test keys and simulator addresses are free. Check them at any time:

bash
curl -H "Authorization: Bearer $KEY" https://api.trivsend.com/usage
json
{
  "object": "usage",
  "plan": { "slug": "starter", "name": "Starter" },
  "status": "active",
  "emails": {
    "daily":   { "used": 132,  "limit": 1000,  "resets_at": "2026-10-23T00:00:00.000Z" },
    "monthly": { "used": 4210, "limit": 30000, "resets_at": "2026-11-01T00:00:00.000Z" }
  }
}

(Shortened; the response also reports contacts, domains and bounce/complaint counts.)

Domains and DNS

You can only send from domains you have added and verified. Add one in the dashboard or with POST /domains {"name": "notify.example.com"}; the response lists the DNS records to publish:

RecordTypeNameValue
DKIMTXTtrivsend._domainkey.notify.example.comv=DKIM1; k=rsa; p=... (unique per domain)
Return pathMXbounces.notify.example.commail.trivsend.com, priority 10
SPFTXTbounces.notify.example.comv=spf1 include:_spf.trivsend.com ~all
DMARC (recommended)TXT_dmarc.notify.example.comv=DMARC1; p=none;

Then verify with Verify in the dashboard or POST /domains/:id/verify. The domain's status becomes verified once DKIM, the return-path MX and SPF are found. DNS changes can take a few minutes to an hour to appear.

Notes:

Domain settings, with PATCH /domains/:id:

FieldEffect
open_tracking, click_trackingtrue/false. Off by default
tracking_subdomaine.g. "links": tracked links use links.notify.example.com instead of a TrivSend host. Add the CNAME it shows (to track.trivsend.com) and verify. null removes it
inboundtrue to receive mail. See Receiving email

Templates

Store an email once, send it by id or alias with different variables. Create templates in the dashboard (Templates) or with POST /templates:

json
{
  "name": "Sign-in code",
  "alias": "otp",
  "from": "Acme <noreply@notify.example.com>",
  "subject": "Your code is {{{CODE}}}",
  "html": "<p>Hi {{{NAME|there}}}, your code is <strong>{{{CODE}}}</strong>.</p>",
  "variables": [{ "key": "CODE", "type": "string" }, { "key": "NAME", "type": "string", "fallback_value": "there" }]
}

Publish it (POST /templates/otp/publish, or Publish in the dashboard), then send:

json
{
  "to": "user@example.com",
  "template": { "id": "otp", "variables": { "CODE": "482913", "NAME": "Asha" } }
}

Webhooks

Webhooks tell your app what happened after a send, without polling. Create one in the dashboard (Webhooks) or with the API:

bash
curl -X POST https://api.trivsend.com/webhooks \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"endpoint": "https://example.com/hooks/trivsend", "events": ["email.sent", "email.bounced", "email.complained"]}'
json
{ "object": "webhook", "id": "...", "signing_secret": "whsec_..." }

Store signing_secret; it is shown once (rotate it with POST /webhooks/:id/signing-secret/rotate).

Events

EventWhen
email.scheduledAn email was scheduled
email.sentThe recipient's server accepted it
email.delivery_delayedA temporary failure; TrivSend keeps retrying
email.bouncedThe address doesn't exist or rejected it; the address is now suppressed
email.complainedThe recipient marked it as spam; the address is now suppressed
email.failedGave up after all retries
email.suppressedNot sent because the recipient is suppressed
email.opened, email.clickedWith open/click tracking on
email.receivedInbound mail arrived. See Receiving email
contact.created, contact.updated, contact.deleted, contact.topics.updatedMarketing contacts changed
domain.created, domain.updated, domain.deletedDomains changed
suppression.added, suppression.removedThe suppression list changed
topic.created, topic.updated, topic.deletedTopics changed

Payload:

json
{
  "type": "email.bounced",
  "created_at": "2026-10-22T09:15:10.000Z",
  "data": {
    "email_id": "3f2a1c9e-7b4d-4e8a-9c1f-2d6b8a0e5f71",
    "created_at": "2026-10-22T09:14:03.120Z",
    "message_id": "<3f2a1c9e-...@notify.example.com>",
    "from": "Acme <noreply@notify.example.com>",
    "to": ["user@example.com"],
    "subject": "Your sign-in code",
    "tags": { "category": "otp" },
    "bounce": {
      "type": "Permanent",
      "subType": "hard",
      "message": "550 5.1.1 user unknown",
      "status": "5.1.1",
      "recipient": "user@example.com"
    }
  }
}

Verify the signature

Every request is signed with the Standard Webhooks scheme. The headers are webhook-id, webhook-timestamp and webhook-signature (also sent as svix-id, svix-timestamp, svix-signature). Any Standard Webhooks or Svix library verifies them. Without a library:

  1. Take the raw request body, exactly as received, before any JSON parsing.
  2. Compute HMAC-SHA256 over <webhook-id>.<webhook-timestamp>.<raw body>, keyed with the base64-decoded part of the secret after whsec_.
  3. Base64 the result and compare it with each v1,<signature> entry in webhook-signature (space-separated), in constant time.
  4. Reject timestamps more than 5 minutes from now, to stop replays.

Node.js (Express):

js
import crypto from "node:crypto";
import express from "express";

const secret = Buffer.from(process.env.TRIVSEND_WEBHOOK_SECRET.replace(/^whsec_/, ""), "base64");

app.post("/hooks/trivsend", express.raw({ type: "application/json" }), (req, res) => {
  const id = req.header("webhook-id");
  const timestamp = req.header("webhook-timestamp");
  const signatures = (req.header("webhook-signature") ?? "").split(" ");
  if (!id || !timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(400);

  const expected = crypto.createHmac("sha256", secret).update(`${id}.${timestamp}.${req.body}`).digest();
  const valid = signatures.some((s) => {
    const given = Buffer.from(s.replace(/^v1,/, ""), "base64");
    return given.length === expected.length && crypto.timingSafeEqual(given, expected);
  });
  if (!valid) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString("utf8"));
  // Deduplicate on webhook-id: a retried delivery has the same id.
  handle(event).then(() => res.sendStatus(200), () => res.sendStatus(500));
});

Python (Flask):

python
import base64, hashlib, hmac, os, time
from flask import Flask, request, abort

secret = base64.b64decode(os.environ["TRIVSEND_WEBHOOK_SECRET"].removeprefix("whsec_"))

@app.post("/hooks/trivsend")
def trivsend_hook():
    msg_id = request.headers.get("webhook-id", "")
    ts = request.headers.get("webhook-timestamp", "0")
    if abs(time.time() - int(ts)) > 300:
        abort(400)
    body = request.get_data()  # raw bytes
    expected = base64.b64encode(hmac.new(secret, f"{msg_id}.{ts}.".encode() + body, hashlib.sha256).digest()).decode()
    given = [s.split(",", 1)[-1] for s in request.headers.get("webhook-signature", "").split()]
    if not any(hmac.compare_digest(expected, g) for g in given):
        abort(401)
    event = request.get_json()
    # ... handle event["type"], deduplicate on msg_id
    return "", 200

Delivery and retries

Receiving email

Turn on inbound for a domain (PATCH /domains/:id {"inbound": true}, or the switch on the domain's page), then add the MX record it shows:

TypeNameValue
MXinbound.notify.example.commail.trivsend.com, priority 10

After verification, mail to any address at inbound.notify.example.com (for example support@inbound.notify.example.com or reply+ticket-42@inbound.notify.example.com) is stored and sent to your email.received webhooks. Messages can be up to 25 MB.

The webhook carries the metadata; fetch the body and attachments with the API:

bash
curl -H "Authorization: Bearer $KEY" https://api.trivsend.com/emails/receiving/$EMAIL_ID
json
{
  "object": "email",
  "id": "...",
  "from": "Asha <asha@example.org>",
  "to": ["support@inbound.notify.example.com"],
  "cc": [],
  "reply_to": [],
  "subject": "Re: Ticket 42",
  "message_id": "<CAF...@mail.gmail.com>",
  "created_at": "2026-10-22T10:01:00.000Z",
  "rcpt_to": ["support@inbound.notify.example.com"],
  "html": "<div>Thanks, that fixed it.</div>",
  "text": "Thanks, that fixed it.",
  "headers": { "in-reply-to": "<...>" },
  "attachments": [
    { "id": "...", "filename": "screenshot.png", "content_type": "image/png", "content_disposition": "attachment", "content_id": null, "size": 48213 }
  ]
}

Download an attachment with GET /emails/receiving/:id/attachments/:attachmentId; content is base64. List everything received with GET /emails/receiving.

Tip: to route replies to the right record, send with reply_to: "reply+ticket-42@inbound.notify.example.com" and read the part after + from rcpt_to when the reply arrives.

Validating addresses

Check an address before you store it or send to it, for example on a sign-up form:

bash
curl -X POST https://api.trivsend.com/validations \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"email": "asha@gmial.com"}'
json
{
  "object": "validation",
  "email": "asha@gmial.com",
  "result": "valid",
  "reason": "accepted",
  "did_you_mean": "asha@gmail.com",
  "checks": { "syntax": true, "mail_server": true, "disposable": false, "role": false, "free": false, "suppressed": false }
}
resultreasonSuggested handling
validacceptedAccept. If did_you_mean is set, ask "Did you mean ...?"
riskydisposable, role_addressAccept or warn, your choice
invalidinvalid_syntax, no_mail_server, suppressedReject or ask for another address
unknowndns_errorAccept; try again later

POST /validations/batch takes {"emails": [...]} (up to 100) and returns { "object": "list", "data": [...] }. Sending-access keys may call both. Validation checks syntax, DNS and lists; it never connects to the mailbox, so valid means "this domain takes mail and nothing marks it risky", not "this inbox exists".

Suppressions

TrivSend never sends to an address on your suppression list. Hard bounces, spam complaints and unsubscribes are added automatically; an email to only suppressed recipients gets status suppressed.

RequestPurpose
GET /suppressions?origin=bounceList (origins: bounce, complaint, unsubscribe, manual)
POST /suppressions {"email": "..."}Block an address yourself
GET /suppressions/:idOrEmailIs this address suppressed, and why
DELETE /suppressions/:idOrEmailRemove one, for example after a user fixed their mailbox
POST /suppressions/batch/add, /batch/removeUp to 100 at once

Remove a complaint suppression only if the person asked for your mail again.

Testing without sending

Two tools, usable together:

Simulator addresses work with any key and never leave TrivSend:

Send toResult
delivered@simulator.trivsend.comsent
bounced@simulator.trivsend.comsent, then a hard bounce and an email.bounced webhook
complained@simulator.trivsend.comsent, then a complaint and an email.complained webhook

Add a label to tell tests apart: bounced+signup-test@simulator.trivsend.com. Simulated events never add suppressions and don't count toward quotas.

Test keys (ts_test_..., created with Test under API keys or POST /api-keys {"name": "ci", "test": true}) accept, log and simulate every email, even to real addresses, and nothing is delivered. They work before your domain is verified and while your account is in review, which makes them right for CI and staging.

SMTP relay

For software that can only send over SMTP (WordPress, Supabase Auth, Keycloak, Grafana, Oracle APEX instance settings, ...):

SettingValue
Hostsmtp.trivsend.com
Port587 with STARTTLS, or 465 with TLS
Usernametrivsend
Passworda TrivSend API key (sending access is enough)

The From domain must be verified, as with the API. Optional headers: X-TrivSend-Tags: category=otp, app=crm and X-TrivSend-Idempotency-Key: otp-1234. The server replies 250 OK queued as <email id>, and the email appears in the log like any API send.

Using the Resend SDKs

TrivSend's sending API matches Resend's request and response shapes, so code written for Resend works by changing the base URL and key. With the official Node SDK, set two environment variables and change nothing else:

bash
RESEND_API_KEY=ts_...
RESEND_BASE_URL=https://api.trivsend.com
js
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({ from: "Acme <noreply@notify.example.com>", to: "user@example.com", subject: "Hi", text: "Hello" });

resend.webhooks.verify() also accepts TrivSend signatures. TrivSend-only fields (test, the extra search filters, validation, inbound) are plain HTTP and are documented above.

Marketing: contacts and broadcasts

For newsletters and announcements (full-access key):

Every broadcast gets one-click List-Unsubscribe headers and an unsubscribe link, unless your HTML places {{{RESEND_UNSUBSCRIBE_URL}}} itself. Contact properties can be used as {{{key}}}. Keep marketing on a different sending subdomain (for example news.example.com) from your transactional mail.

Best practices

Endpoint reference

All paths are relative to https://api.trivsend.com.

Method and pathKeyPurpose
POST /emailssendingSend one email
POST /emails/batchsendingSend up to 100 emails
GET /emailsfullList and search sent email
GET /emails/:idfullOne email and its last event
PATCH /emails/:idfullReschedule a scheduled email
POST /emails/:id/cancelfullCancel a scheduled email
GET /emails/receivingfullList received email
GET /emails/receiving/:idfullOne received email with its body
GET /emails/receiving/:id/attachments/:attachmentIdfullDownload an attachment (base64)
POST /validationssendingValidate one address
POST /validations/batchsendingValidate up to 100 addresses
GET /usageanyPlan, quotas and usage
GET /stats?start=&end=&stream=fullDaily counts and rates (dates YYYY-MM-DD, up to 92 days)
POST /domains, GET /domainsfullAdd, list domains
GET, PATCH, DELETE /domains/:idfullInspect (with DNS records), change settings, remove
POST /domains/:id/verifyfullCheck DNS now
POST /api-keys, GET /api-keysfullCreate (token shown once), list
PATCH, DELETE /api-keys/:idfullRename, revoke
POST /templates, GET /templatesfullCreate, list templates
GET, PATCH, DELETE /templates/:idOrAliasfullRead, edit draft, delete
POST /templates/:idOrAlias/publish, /duplicatefullPublish, copy
POST /webhooks, GET /webhooksfullCreate, list webhooks
GET, PATCH, DELETE /webhooks/:idfullInspect, change (endpoint, events, status), remove
POST /webhooks/:id/signing-secret/rotatefullNew signing secret
GET /webhooks/:id/events[/:eventId]fullDelivery log
POST /webhooks/:id/events/:eventId/replayfullSend an event again
POST /suppressions, GET /suppressionsfullAdd, list
GET, DELETE /suppressions/:idOrEmailfullCheck, remove
POST /suppressions/batch/add, /batch/removefullUp to 100 at once
POST /contacts, GET /contactsfullCreate or update, list contacts
GET, PATCH, DELETE /contacts/:idOrEmailfullRead, change, delete
GET /contacts/:idOrEmail/segments, POST/DELETE .../segments/:segmentIdfullA contact's segments
GET, PATCH /contacts/:idOrEmail/topicsfullA contact's topic subscriptions
POST /segments, GET /segments, GET/PATCH/DELETE /segments/:id, GET /segments/:id/contactsfullSegments
POST /topics, GET /topics, GET/PATCH/DELETE /topics/:idfullTopics
POST /broadcasts, GET /broadcasts, GET/PATCH/DELETE /broadcasts/:idfullBroadcasts
POST /broadcasts/:id/send, /cancel, /duplicatefullSend (now or scheduled_at), cancel, copy
GET /healthnoneService health

Instructions for AI coding assistants

If you are an AI assistant adding TrivSend to a codebase, follow these rules:

  1. Read the key from an environment variable (TRIVSEND_API_KEY). Never hard-code it, never log it, never send it to a browser.
  2. Call POST https://api.trivsend.com/emails from server-side code only, with Authorization: Bearer <key> and Content-Type: application/json.
  3. Put the from address on a domain the user has verified in TrivSend. If you don't know it, read it from configuration (for example TRIVSEND_FROM) and ask the user for the value.
  4. Send both html and text.
  5. Set an Idempotency-Key derived from the business event (for example password-reset-<token id>), and reuse it on retries.
  6. Treat 200 with an id as "queued", not "delivered". Store the id if the app needs to track delivery.
  7. On errors, read name from the JSON body. Retry only network errors, 429 rate_limit_exceeded and 5xx, with backoff.
  8. For delivery outcomes, add a webhook endpoint that verifies the signature over the raw body (see Verify the signature) and returns 2xx quickly.
  9. In tests, use a ts_test_ key or delivered@simulator.trivsend.com; never send real email from CI.
  10. Don't invent endpoints or fields. Everything TrivSend supports is in this guide.

Support