Bytloop MailDocs
Status

Getting started

  • Introduction
  • Quickstart

Sending email

  • Send a transactional email
  • Sending domains vs mailboxes

Framework guides

  • Send email from Next.js
  • Send email from Ruby on Rails
  • Send email from Django
  • Send email from Laravel

Domains

  • Verify a domain

Receiving email

  • Receive email
  • Webhooks

Reference

  • Rate limits
  • Errors
  • SDKs

Framework guides

Send email from Next.js

Send transactional email from a Next.js App Router Route Handler or Server Action, without exposing your API key to the client.

Try it live

Every endpoint in this doc runs against your workspace from the interactive API explorer. Prefill a request, hit Send, and see the response with your own keys.

This is the same POST /api/v1/emails endpoint documented in Send a transactional email, wired into the two places a Next.js App Router app actually calls it from: a Route Handler and a Server Action.

Never call the API from a Client Component

Your Bytloop Mail API key must stay server-side. Calling /api/v1/emails directly from a Client Component — or storing the key in a NEXT_PUBLIC_* env var — ships it to every visitor's browser. Always go through a Route Handler or Server Action, and keep the key as a plain (non-public) environment variable.

Route Handler#

Good fit when the caller isn't a form on the same page — a webhook you relay, a cron job, a mobile client hitting your Next.js app as a backend.

// app/api/send-receipt/route.ts
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const { orderId, customerEmail } = await request.json();

  const response = await fetch("https://mail.bytloop.com/api/v1/emails", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.BYTLOOP_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `receipt-${orderId}`,
    },
    body: JSON.stringify({
      from: "Support <support@send.acme.com>",
      to: [customerEmail],
      subject: `Your receipt for order #${orderId}`,
      html: `<p>Thanks for the order! Order #${orderId} is confirmed.</p>`,
      tags: { receipt_for_order: String(orderId) },
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    return NextResponse.json({ error: error.message }, { status: 502 });
  }

  const { id } = await response.json();
  return NextResponse.json({ id });
}
// app/api/send-receipt/route.ts
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const { orderId, customerEmail } = await request.json();

  const response = await fetch("https://mail.bytloop.com/api/v1/emails", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.BYTLOOP_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `receipt-${orderId}`,
    },
    body: JSON.stringify({
      from: "Support <support@send.acme.com>",
      to: [customerEmail],
      subject: `Your receipt for order #${orderId}`,
      html: `<p>Thanks for the order! Order #${orderId} is confirmed.</p>`,
      tags: { receipt_for_order: String(orderId) },
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    return NextResponse.json({ error: error.message }, { status: 502 });
  }

  const { id } = await response.json();
  return NextResponse.json({ id });
}

Server Action#

Good fit when the send is triggered by a form submission or a button in a Server Component tree — no separate API route to wire up.

// app/orders/[id]/actions.ts
"use server";

export async function sendReceipt(orderId: string, customerEmail: string) {
  const response = await fetch("https://mail.bytloop.com/api/v1/emails", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.BYTLOOP_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `receipt-${orderId}`,
    },
    body: JSON.stringify({
      from: "Support <support@send.acme.com>",
      to: [customerEmail],
      subject: `Your receipt for order #${orderId}`,
      html: `<p>Thanks for the order! Order #${orderId} is confirmed.</p>`,
      tags: { receipt_for_order: String(orderId) },
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Send failed: ${error.message}`);
  }

  return response.json() as Promise<{ id: string }>;
}
// app/orders/[id]/actions.ts
"use server";

export async function sendReceipt(orderId: string, customerEmail: string) {
  const response = await fetch("https://mail.bytloop.com/api/v1/emails", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.BYTLOOP_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `receipt-${orderId}`,
    },
    body: JSON.stringify({
      from: "Support <support@send.acme.com>",
      to: [customerEmail],
      subject: `Your receipt for order #${orderId}`,
      html: `<p>Thanks for the order! Order #${orderId} is confirmed.</p>`,
      tags: { receipt_for_order: String(orderId) },
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Send failed: ${error.message}`);
  }

  return response.json() as Promise<{ id: string }>;
}
// app/orders/[id]/resend-button.tsx
"use client";

import { useTransition } from "react";
import { sendReceipt } from "./actions";

export function ResendButton({
  orderId,
  customerEmail,
}: {
  orderId: string;
  customerEmail: string;
}) {
  const [isPending, startTransition] = useTransition();

  return (
    <button
      disabled={isPending}
      onClick={() => startTransition(() => sendReceipt(orderId, customerEmail))}
    >
      {isPending ? "Sending…" : "Resend receipt"}
    </button>
  );
}
// app/orders/[id]/resend-button.tsx
"use client";

import { useTransition } from "react";
import { sendReceipt } from "./actions";

export function ResendButton({
  orderId,
  customerEmail,
}: {
  orderId: string;
  customerEmail: string;
}) {
  const [isPending, startTransition] = useTransition();

  return (
    <button
      disabled={isPending}
      onClick={() => startTransition(() => sendReceipt(orderId, customerEmail))}
    >
      {isPending ? "Sending…" : "Resend receipt"}
    </button>
  );
}

Environment variable#

# .env.local — never prefix with NEXT_PUBLIC_
BYTLOOP_API_KEY=bm_live_xxxxxxxxxxxxxxxx
# .env.local — never prefix with NEXT_PUBLIC_
BYTLOOP_API_KEY=bm_live_xxxxxxxxxxxxxxxx

Common pitfalls#

  • Using NEXT_PUBLIC_BYTLOOP_API_KEY. Anything prefixed NEXT_PUBLIC_ is inlined into the client bundle at build time. Use a plain env var name and only read it in a Route Handler, Server Action, or other server-only file.
  • No idempotency key on a Server Action. Server Actions re-run on React's automatic retry after a network blip, which can double-send. Pass a stable Idempotency-Key (order id, invoice id — whatever is unique to the thing being emailed) exactly as in the examples above.
  • Edge runtime. If the Route Handler exports export const runtime = "edge", process.env still works for server-only vars set in your hosting provider's dashboard, but local .env.local values need next dev (not next dev --turbo on very old Next versions had a known env-loading gap) — if the key comes back undefined locally, check it's actually loaded with a console.log before debugging the API call itself.

Next#

  • Register a webhook endpoint — also a Next.js Route Handler — to hear about delivery, bounces, and opens.
  • Read the rate limits page so you know how the platform behaves under bursts (e.g. a newsletter send from a cron job).
PreviousSending domains vs mailboxesNextSend email from Ruby on Rails

On this page

  • Route Handler
  • Server Action
  • Environment variable
  • Common pitfalls
  • Next