Notix
Use cases

Order confirmations and receipts that send exactly once.

An order confirmation is the email a customer reads most carefully, and the one your server is most likely to send twice: a timeout, a retry, a webhook delivered again. One template through the transactional email API, one idempotency key per order and purpose, and delivery events back through a signed webhook. Receipts use the same call with a different template and key.

The flow

Confirm, send, hear back.

Three steps and one column on your order table. The email carries the order’s values as variables; Notix carries the email and tells you what happened to it.

  1. Wait for the payment to settle.

    Send from the payment provider’s success event, not from the page that showed the total. That is the moment the order exists in a form worth confirming, and it is also the event that tends to arrive twice.

  2. Send with a key that names the order.

    One call to POST /v1/emails with the template, the formatted order values, and Idempotency-Key: order-{orderId}-confirmation. A retry returns the same emailId; store it on the order row.

  3. Update the order from the webhook.

    email.delivered marks the confirmation sent. email.bounced with a permanent type means the address is wrong and is now suppressed; flag the order for a new address rather than resending.

Code

Send the confirmation.

curl, or the TypeScript and Python SDKs. One template with six variables; a receipt is the same call with tpl_order_receipt and the key order-{orderId}-receipt.

curl
# One template, tpl_order_confirmation, with variables for the order.
# The idempotency key names the order and the purpose, so a retried call
# after a timeout returns the original emailId instead of a second email.
curl -X POST https://app.usenotix.dev/api/v1/emails \
  -H "Authorization: Bearer $NOTIX_API_KEY" \
  -H "Idempotency-Key: order-48213-confirmation" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <orders@acme.com>",
    "to": "chidi@example.com",
    "replyTo": "support@acme.com",
    "templateId": "tpl_order_confirmation",
    "variables": {
      "orderNumber": "48213",
      "customerName": "Chidi",
      "items": "2 x Desk lamp (NGN 24,000), 1 x Bulb pack (NGN 3,500)",
      "total": "NGN 27,500",
      "deliveryDate": "Thursday 18 September",
      "orderUrl": "https://acme.com/orders/48213"
    }
  }'

# 200 { "emailId": "eml_abc123" }
# Same key, same body again: 200 with the same emailId, no second email.
# Same key, different body: 409 NOT_UNIQUE.

Hear back.

A webhook handler that finds the order by the stored emailId and records what happened. Acknowledge first, then do the work; the signature check is one SDK call.

TypeScript
// POST /hooks/notix  (Express: app.post("/hooks/notix", express.raw({ type: "*/*" }), handler))
import { Notix } from "notix-js";

const notix = new Notix(process.env.NOTIX_API_KEY);
const webhooks = notix.webhooks(process.env.NOTIX_WEBHOOK_SECRET);

export async function handler(req, res) {
  // Verifies X-Notix-Signature over the timestamp and the raw body, with a
  // five-minute tolerance. Throws on a bad or stale signature.
  const event = webhooks.constructEvent(req.body, req.headers);
  res.status(200).end(); // acknowledge first, then work

  const order = await db.order.findFirst({ where: { confirmationEmailId: event.data.id } });
  if (!order) return;

  switch (event.type) {
    case "email.delivered":
      await db.order.update({ where: { id: order.id }, data: { confirmationStatus: "sent" } });
      break;
    case "email.bounced":
      // Permanent: the address is now suppressed; ask the customer for another one.
      // Transient: the mailbox was full or the server was busy; nothing to do yet.
      if (event.data.bounce.type === "Permanent") {
        await db.order.update({ where: { id: order.id }, data: { confirmationStatus: "address_invalid" } });
        await flagOrderForContact(order.id);
      }
      break;
    case "email.complained":
      // A receipt marked as spam is rare; do not resend it.
      await db.order.update({ where: { id: order.id }, data: { confirmationStatus: "complained" } });
      break;
  }
}
Rules

What Notix does, and what your app keeps.

The first four are what the send API and the webhooks do on their own; the last three are conventions the page recommends.

RuleEnforced byNotes
One email per order and purposeNotixPass Idempotency-Key: order-{orderId}-confirmation. A retry with the same key and body returns the original emailId; the same key with a different body answers 409 NOT_UNIQUE. Keys are kept for 24 hours.
Receipts reach unsubscribed contactsNotixUnsubscribing is a marketing preference. A send with no campaignId or journeyId is transactional and goes out to a contact who unsubscribed from campaigns.
Suppressed addresses are droppedNotixAn address that hard bounced or complained is not sent. The request still answers 200 with an emailId, and the email's status reads SUPPRESSED.
Delivery events arrive by webhookNotixemail.delivered, email.bounced and email.complained carry the emailId you stored, signed with HMAC-SHA256 over the timestamp and the raw body.
Send after the payment is finalYour appTrigger the confirmation from the payment provider's success event, not from the checkout page. A confirmation for an order that never settled is a support ticket.
Store the emailId on the orderYour appSave emailId next to the order row when the send returns, so support can look up what happened to a specific customer's confirmation.
Numbers are formatted before they reach the templateYour appTemplate variables are strings. Format currency, quantities and dates in your code, in the customer's locale, and pass the finished text.

The send request, the idempotency header, the email status values and the webhook payloads are in the docs. The key semantics are on the idempotency keys page.

Example

What the customer receives.

The order number in the subject, the items and the total in the body, one link to the order, and a reply-to a person reads.

Confirmation

Subject: Your Acme order #48213 is confirmed

Thanks, Chidi. We have your order and will deliver it on Thursday 18 September. 2 x Desk lamp (NGN 24,000), 1 x Bulb pack (NGN 3,500). Total NGN 27,500. View your order. Reply to this email if anything looks wrong.

Receipt

Subject: Receipt for Acme order #48213

Payment of NGN 27,500 received on 12 September by card ending 4242. Order #48213, 3 items. Download your receipt. Keep this email for your records.

Same API call, a different template and the key order-48213-receipt.

Design notes

Six habits that keep confirmations honest.

Send from the payment event.

The confirmation is proof that money moved. Trigger it from the provider's success webhook, so a card that was declined after the checkout page rendered never produces a confirmation.

One key per order and purpose.

order-48213-confirmation and order-48213-receipt are different emails, so they get different keys. A retry of either returns the original id; a changed order with the old key answers 409, which is the right answer.

Format in your code, not in the template.

Variables are strings. Currency symbols, thousands separators, quantities and dates come out of your app already formatted for the customer, so the template stays one file for every locale.

Reply-to a person.

Set replyTo to the address support reads. A customer who replies to a confirmation is asking about the order, and a no-reply mailbox turns that into a lost message.

Treat a permanent bounce as a data problem.

A confirmation that bounces permanently means the order has the wrong address. The address is suppressed from that moment, so a resend to it goes nowhere; ask the customer for another one.

Watch SUPPRESSED, not just the 200.

A send to a suppressed address returns an id and no email. When a customer reports nothing arrived, look the id up before resending: the status says whether it was ever sent.

FAQ

Questions, answered.

How do I send an order confirmation email through an API?
One POST to /api/v1/emails from the payment provider's success event, with a templateId, the order's values as variables, and an Idempotency-Key header naming the order and the purpose. The response carries an emailId; store it on the order. Delivery, bounce and complaint events come back through a signed webhook with that id. The same call sends a receipt; only the template and the key differ.
Is a receipt the same as an order confirmation?
For the API, yes: both are transactional sends to one recipient from one template. The difference is timing and purpose. A confirmation goes out when the order is accepted; a receipt goes out when the payment is captured, which for card payments is usually the same moment and for bank transfers is later. Use one template per purpose and one idempotency key per order and purpose.
What happens if my server retries the send?
Nothing bad, if the key is the same. The idempotency key is kept for 24 hours; a retry with the same key and the same body returns the original emailId and sends nothing. The same key with a different body, for example after the order total changed, answers 409 NOT_UNIQUE, so the second version is never sent under the first version's key. Use a new key when the content is deliberately different.
Will a customer who unsubscribed still get their receipt?
Yes. Unsubscribing applies to marketing sends, which are the ones with a campaignId or a journeyId. A send made directly through the API is transactional and goes to an unsubscribed contact. The only addresses that are not sent are the ones on the suppression list, which holds hard bounces and spam complaints.
How do I show line items in the template?
Template variables are strings, so build the line-item text in your code and pass it as one variable, or pass one variable per line for a fixed number of rows. Format prices, quantities and dates before they reach the template. The template editor in the dashboard shows the variables a template expects, and the send request's variables object fills them.
How do I know the confirmation was delivered?
Add a webhook endpoint for email.delivered, email.bounced and email.complained. Each event carries the emailId from the send response, so your handler can find the order and update its status. Verify the signature over the timestamp and the raw body before trusting the event; the SDKs do it in one call. The email log in the dashboard shows the same timeline for support.

Confirm every order once.

One template, one key per order, delivery events by webhook, and the free plan's 5,000 emails a month to test with. No card.