Notix
Guides

Send email from Deno with one fetch.

Deno ships fetch and denies network and environment access until you allow it, so a Notix send is one POST to the email API and two permission flags. This guide sends from a script, then from a Deno.serve handler, then notes what changes on Deno Deploy. The SDK is available through an npm: specifier when you want it; the request is identical.

What you need

Three things, none of them a card.

An API key.

Created in the dashboard under API keys, shown once. A sending access key is enough for this guide and cannot read contacts or delete domains if it leaks.

A verified domain.

Add your domain, publish the SPF, DKIM and DMARC records Notix gives you, and the from address on it becomes valid. The quickstart walks through it.

Deno, and two flags.

Nothing to install for the fetch path. The run command grants exactly what the script uses:

terminal
# Outbound requests and the environment are both off until you allow them.
deno run --allow-net=app.usenotix.dev --allow-env=NOTIX_API_KEY send.ts

The free plan covers 5,000 emails a month and 200 a day, and the free plan does not ask for a card.

Steps

From an empty file to a queued message.

  1. Read the key with Deno.env.get.

    Export NOTIX_API_KEY in the shell that runs the script, or keep it in a .env file and pass --env-file=.env to deno run. Deno.env.get returns undefined rather than throwing when a variable is missing, so check it before the first send. The permission flag --allow-env=NOTIX_API_KEY exposes that one variable and nothing else.

  2. Send one email.

    One POST to /api/v1/emails with a Bearer token and from, to, subject and html or text (send both; some clients only show one). The Idempotency-Key header makes a retry safe: the same key and body return the original message. The SDK tab is the same call through npm:notix-js, which Deno fetches and caches on first run.

    send.ts
    // No dependency: Deno's fetch and the JSON API are enough.
    const apiKey = Deno.env.get("NOTIX_API_KEY");
    
    const response = await fetch("https://app.usenotix.dev/api/v1/emails", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        // A retried request with the same key returns the original send.
        "Idempotency-Key": "order-4471-receipt",
      },
      body: JSON.stringify({
        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.",
      }),
    });
    
    const body = await response.json();
    
    if (!response.ok) {
      // Every error is { error: { code, message } }.
      console.error(body.error.code, body.error.message);
    } else {
      console.log("queued", body.emailId);
    }
    
  3. Keep the id.

    The response carries the email’s id. Store it next to the order or the user that caused the send: it is the key you read the message back with (GET /api/v1/emails/{id}) and the data.id every webhook event for that message carries.

  4. Send from a Deno.serve handler.

    In an app the send sits behind a route. Deno.serve takes a handler of (request) => Response; match the method and path yourself or with a router. A server needs --allow-net without a host list, or the listening host added to it, because listening is network access too.

    server.ts
    const apiKey = Deno.env.get("NOTIX_API_KEY");
    
    async function sendReceipt(orderId: string, to: string) {
      const response = await fetch("https://app.usenotix.dev/api/v1/emails", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${apiKey}`,
          "Content-Type": "application/json",
          "Idempotency-Key": `order-${orderId}-receipt`,
        },
        body: JSON.stringify({
          from: "receipts@acme.com",
          to,
          subject: `Your receipt for order ${orderId}`,
          html: `<p>Thanks for order ${orderId}.</p>`,
          text: `Thanks for order ${orderId}.`,
        }),
      });
      return { ok: response.ok, body: await response.json() };
    }
    
    Deno.serve(async (request) => {
      const url = new URL(request.url);
    
      if (request.method === "POST" && url.pathname === "/api/receipts") {
        const { orderId, to } = await request.json();
        const { ok, body } = await sendReceipt(orderId, to);
        return Response.json(ok ? { id: body.emailId } : { error: body.error }, {
          status: ok ? 200 : 502,
        });
      }
    
      return new Response("Not found", { status: 404 });
    });
    
    // deno run --allow-net --allow-env=NOTIX_API_KEY server.ts
    // --allow-net without a host list: the server listens and the send goes out.
    
  5. Deploy it.

    On Deno Deploy the handler above runs as it is. Set NOTIX_API_KEY and NOTIX_WEBHOOK_SECRET as environment variables in the project’s settings, not in a file in the repository; Deno.env.get reads them and no permission flags are involved on the platform.

In production

Same three lines as Node.

An idempotency key, a look at the error envelope, and a webhook for delivery status. The samples above already carry the key and check the envelope; the webhook is one more Deno.serve handler that verifies the signature over the raw body. The reasoning behind each is in the Node.js guide and applies unchanged.

webhooks.ts
import { Notix } from "npm:notix-js";

const notix = new Notix(Deno.env.get("NOTIX_API_KEY"));
const webhooks = notix.webhooks(Deno.env.get("NOTIX_WEBHOOK_SECRET"));

Deno.serve(async (request) => {
  if (request.method !== "POST") return new Response("Not found", { status: 404 });

  // Verify over the raw body, not a parsed object.
  const rawBody = await request.text();
  const event = webhooks.constructEvent(rawBody, { headers: request.headers });

  switch (event.type) {
    case "email.delivered":
    case "email.bounced":
    case "email.complained":
      // event.data.id is the emailId you were given at send time.
      break;
  }
  return new Response("ok");
});

The verifier uses createHmac and timingSafeEqual from Node’s crypto module, which Deno implements for npm packages, so constructEvent works here as it does on Node. Without the SDK, the check is HMAC-SHA256 over timestamp.rawBody compared with the X-Notix-Signature header; the signature page shows the raw check step by step. Batch sends, scheduling and templates are in the production guide for transactional email; the runtime does not change any of it.

FAQ

Questions, answered.

Which permissions does a Deno script need to send email?
Two. --allow-net, which you can narrow to --allow-net=app.usenotix.dev so the script can reach nothing else, and --allow-env, narrowed to --allow-env=NOTIX_API_KEY so only the key is readable. Deno blocks network and environment access by default, so without these flags the fetch throws a permission error before any request leaves. A server that listens needs the unrestricted --allow-net or the listening host added to the list.
Should I use the SDK or plain fetch on Deno?
Plain fetch is the natural fit: the API is one JSON POST with a Bearer token, and Deno's fetch is standard, so there is nothing to install or cache. Use the SDK through the npm:notix-js specifier when you want the types, the idempotencyKey option and the webhook verifier. Both send exactly the same request.
Does notix-js work under Deno's npm compatibility?
Yes. The SDK sends with the global fetch and reads the key you pass to the constructor. It also looks at process.env when you pass no key, and Deno provides a process global for npm packages. The webhook verifier imports createHmac and timingSafeEqual from Node's crypto module, which Deno implements, so constructEvent verifies signatures the same way it does on Node.
How do I send from a Deno.serve handler?
Read the key once at module scope with Deno.env.get, then call fetch inside the handler for the route that should send and return Response.json with the email id. Build the Idempotency-Key header from your own record id so a retried request returns the original send instead of creating a second one.
How does this run on Deno Deploy?
The same Deno.serve handler runs unchanged. Set NOTIX_API_KEY and NOTIX_WEBHOOK_SECRET as environment variables in the project's settings on Deno Deploy rather than in a file; Deno.env.get reads them, and there are no permission flags to pass because the platform grants network and environment access to the deployment.
Can I use the SMTP relay from Deno instead?
Yes, with any SMTP client that runs under Deno: host smtp.usenotix.dev, port 465 with implicit TLS or 587 with STARTTLS, username notix, and an API key as the password. You lose the idempotency key and the batch endpoint, which only the API exposes, but every message still lands in the same log with the same suppression and webhooks. For Deno the API is usually the shorter path, since fetch needs no client at all.

Send the first one now.

Verify a domain, copy an API key, deno run with two flags. The free plan does not ask for a card.