An API key in the environment.
Created under API keys and shown once. Export it as NOTIX_API_KEY in the shell; every example reads it from there so the key never lands in a script or a shell history line.
The whole email API fits in one HTTP request, which makes curl the fastest way to see it work and the right way to read the contract before you write a client. This guide sends a real email from the terminal, reads the error envelope, and turns the call into a bash script that retries without ever sending twice.
Created under API keys and shown once. Export it as NOTIX_API_KEY in the shell; every example reads it from there so the key never lands in a script or a shell history line.
The from address has to be on a domain whose records you have published. The quickstart walks through it; the SPF, DKIM and DMARC page explains what each record proves.
Authorization: Bearer carries the key, Content-Type: application/json tells the API how to read the body, and Idempotency-Key names the business event so a repeat of this exact call returns the same email instead of a second one. The body is from, to and either a subject with html or text (send both), or a templateId with variables.
curl -X POST https://app.usenotix.dev/api/v1/emails \
-H "Authorization: Bearer $NOTIX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4471-receipt" \
-d '{
"from": "receipts@acme.com",
"to": "customer@example.com",
"subject": "Your receipt for order 4471",
"html": "<p>Thanks for your order.</p>",
"text": "Thanks for your order."
}'
# 200 { "emailId": "eml_01j9x2mz" }
# A template made in the dashboard, with its variables filled from the call.
curl -X POST https://app.usenotix.dev/api/v1/emails \
-H "Authorization: Bearer $NOTIX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4471-receipt" \
-d '{
"from": "receipts@acme.com",
"to": "customer@example.com",
"templateId": "tpl_receipt_v4",
"variables": { "orderId": "4471", "total": "NGN 12,400" }
}'
# 200 { "emailId": "eml_01j9x2mz" }
A 200 answers with the email’s id. It is how you read the message back and the data.id on every webhook event for it.
# Read a message back by the id the send returned.
curl https://app.usenotix.dev/api/v1/emails/eml_01j9x2mz \
-H "Authorization: Bearer $NOTIX_API_KEY"
Every failure is one shape, { error: { code, message } }, and the status matches the code. The codes below are the complete list the API can answer with; the two you will meet first are BAD_REQUEST for an unverified from domain and UNAUTHORIZED for a key that did not make it into the header.
# The key is missing or wrong.
# 401 { "error": { "code": "UNAUTHORIZED", "message": "..." } }
# The from address is on a domain you have not verified, or a field is over
# its limit. The message names the field.
# 400 { "error": { "code": "BAD_REQUEST", "message": "..." } }
# A sending-access key reached an endpoint it cannot use, or the key is
# restricted to another domain.
# 403 { "error": { "code": "FORBIDDEN", "message": "..." } }
# The same Idempotency-Key with a different body, or the first request is
# still in flight.
# 409 { "error": { "code": "NOT_UNIQUE", "message": "..." } }
# Too many requests. The headers say when to try again.
# 429 { "error": { "code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again in 60 seconds." } }
# Retry-After: 60
# X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset
# A one-time code refused by risk scoring (the verify endpoints only).
# 422 { "error": { "code": "RISK_REFUSED", "message": "..." } }
# A wallet team with no balance for an SMS.
# 402 { "error": { "code": "INSUFFICIENT_BALANCE", "message": "..." } }
# Over 20 MB. Refused at the edge, so there is no JSON body at all.
# 413
Three habits turn the one-liner into something a cron job can run: set -euo pipefail so a missing variable stops the script, --fail-with-body so a 4xx exits non-zero but still shows the envelope, and one Idempotency-Key reused on every attempt so the retry loop is safe by construction.
#!/usr/bin/env bash
# send-receipt.sh <order-id> <to>
# Sends one receipt. Safe to run twice: the same order id is the same
# Idempotency-Key, so a retry returns the original email instead of a second one.
set -euo pipefail
: "${NOTIX_API_KEY:?set NOTIX_API_KEY in the environment, never in the script}"
ORDER_ID="$1"
TO="$2"
BODY=$(cat <<JSON
{
"from": "receipts@acme.com",
"to": "$TO",
"templateId": "tpl_receipt_v4",
"variables": { "orderId": "$ORDER_ID" }
}
JSON
)
for attempt in 1 2 3; do
# --fail-with-body: exit non-zero on 4xx/5xx but still print the error envelope.
if RESPONSE=$(curl --silent --show-error --fail-with-body \
-X POST https://app.usenotix.dev/api/v1/emails \
-H "Authorization: Bearer $NOTIX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-$ORDER_ID-receipt" \
-d "$BODY"); then
echo "queued: $RESPONSE"
exit 0
fi
# Only a 429 or a 5xx is worth retrying; a 400, 401, 403 or 409 will not
# change on the next try. Read the code out of the envelope.
CODE=$(printf '%s' "$RESPONSE" | sed -n 's/.*"code":"\([A-Z_]*\)".*/\1/p')
case "$CODE" in
RATE_LIMITED|INTERNAL_SERVER_ERROR|SERVICE_UNAVAILABLE|"")
echo "attempt $attempt failed ($CODE), retrying" >&2
sleep $((attempt * 5))
;;
*)
echo "not retrying: $RESPONSE" >&2
exit 1
;;
esac
done
echo "gave up after 3 attempts" >&2
exit 1
A RATE_LIMITED answer carries Retry-After in seconds and the X-RateLimit-* headers; a 5xx is transient. A 400, 401, 403 or 409 will answer the same way on the next attempt, so the script stops on those and prints the envelope for a person.
Because the key is order-<id>-receipt, a second run of the script for the same order, or a retry after a timeout whose request did get through, returns the original id. The idempotency page has the exact rules: 24 hours, 256 characters, 409 on a different body.
POST /api/v1/emails/batch takes an array of the same objects. Each item carries the single-send limits; the batch as a whole is capped at 20 MB of body and 40 MB of attachments once decoded. Items can carry their own scheduledAt. Batch sends and scheduling are covered in full on the batch and scheduled sends page.
# Up to 100 emails in one call; one Idempotency-Key for the whole batch.
curl -X POST https://app.usenotix.dev/api/v1/emails/batch \
-H "Authorization: Bearer $NOTIX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: statements-2026-09" \
-d '[
{ "from": "statements@acme.com", "to": "ada@example.com",
"templateId": "tpl_statement", "variables": { "month": "September" } },
{ "from": "statements@acme.com", "to": "tunde@example.com",
"templateId": "tpl_statement", "variables": { "month": "September" },
"scheduledAt": "2026-10-01T08:00:00+01:00" }
]'
# 200 { "data": [ { "emailId": "eml_1" }, { "emailId": "eml_2" } ] }
For the production reasoning behind idempotency keys, the error envelope and webhooks, read the Node.js guide; the contract is the same whatever sends the request.
One JSON API for transactional and marketing email: send, batch, schedule, webhooks, typed SDKs.
GuidesThe SDK, plain fetch, or Nodemailer pointed at the relay: three ways to send from Node in a few minutes.
LearnWhy a retried request must not send twice, how the Idempotency-Key header works, and how to choose a key.
NotixThe quickstart: verify a domain, copy an API key, send the first email.
NotixA free plan with no card, and Pro at $15 a month for 50,000 emails. Only sent volume is metered.
Verify a domain, copy an API key, paste the curl call. The free plan does not ask for a card.