Skip to content

Quickstart

Send your first email.

Four starting points for sending transactional email with Mailfully. The Next.js and Express snippets use the official @mailfully/node SDK and are typechecked against its real types; Rails and Laravel call the API directly.

Next.js

Typechecked SDK snippet

Call the SDK from a Route Handler or Server Action. Branch on error — the SDK never throws — and return the queued id.

app/api/welcome/route.tsts
// Quickstart fixture — Next.js Route Handler / Server Action.
//
// This is a REAL, copy-pasteable `@mailfully/node` script. It is typechecked
// against the published SDK types by the marketing app's `tsc` gate (it lives
// under `src`). The docs pages render this file's SOURCE TEXT verbatim — they
// never IMPORT it — so the SDK is shown exactly as a developer would write it
// while staying entirely out of the Next bundle.
import { Mailfully } from "@mailfully/node";

const mailfully = new Mailfully({
  apiKey: process.env.MAILFULLY_API_KEY ?? "", // never hard-code your key
});

const { data, error } = await mailfully.emails.send({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Welcome aboard",
  html: "<p>Thanks for signing up!</p>",
});

if (error) {
  // No method ever throws — failures arrive on the error arm.
  console.error(`Send failed (${error.statusCode}):`, error.message);
} else {
  console.log("Queued email id:", data.id);
}

Express

Typechecked SDK snippet

A POST handler that sends with an idempotency key (safe to retry) and maps the SDK result onto the HTTP response.

routes/welcome.tsts
// Quickstart fixture — Express route handler.
//
// A REAL `@mailfully/node` script, typechecked against the SDK by the gate's
// `tsc`. Shown verbatim in the docs (read as text, never imported), so neither
// the SDK nor Express is pulled into the marketing bundle. The handler is typed
// against a minimal structural view of Express's `req`/`res` so the fixture
// stays dependency-free while still demonstrating the real integration shape —
// in your app you would `import express from "express"` and get these types for
// free. The handler branches on `error` and maps the SDK result onto an HTTP
// response.
import { Mailfully } from "@mailfully/node";

// Minimal structural stand-ins for Express's request/response (your real app
// gets these from `@types/express`). They keep this fixture self-contained.
interface ExpressRequest {
  body: { userId: string };
}
interface ExpressResponse {
  status(code: number): ExpressResponse;
  json(payload: unknown): void;
}

const mailfully = new Mailfully({
  apiKey: process.env.MAILFULLY_API_KEY ?? "",
});

// app.post("/welcome", welcomeHandler) — wire this into your Express app.
async function welcomeHandler(
  req: ExpressRequest,
  res: ExpressResponse,
): Promise<void> {
  const { data, error } = await mailfully.emails.send(
    {
      from: "[email protected]",
      to: "[email protected]",
      subject: "Welcome aboard",
      html: "<p>Thanks for signing up!</p>",
      replyTo: "[email protected]",
    },
    // An idempotency key makes the send safe to retry: a replay returns the
    // ORIGINAL `{ id }` instead of sending a duplicate.
    { idempotencyKey: `welcome:${req.body.userId}` },
  );

  if (error) {
    // The SDK never throws; surface the API's status to the caller.
    res.status(error.statusCode ?? 502).json({ error: error.message });
    return;
  }

  res.status(202).json({ id: data.id });
}

const handler: (req: ExpressRequest, res: ExpressResponse) => Promise<void> =
  welcomeHandler;
console.log("welcome handler ready:", typeof handler);

Rails (Ruby)

Direct HTTP

No Ruby SDK ships yet, so call the API directly with the standard library. The request shape is identical to the SDK's.

app/services/welcome_mailer.rbruby
# Gemfile already includes the standard library 'net/http' + 'json'.
require "net/http"
require "json"
require "uri"

uri = URI("https://api.mailfully.com/v1/emails")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('MAILFULLY_API_KEY')}"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = "welcome:#{user.id}" # safe to retry
request.body = {
  from: "[email protected]",
  to: "[email protected]",
  subject: "Welcome aboard",
  html: "<p>Thanks for signing up!</p>"
}.to_json

response = http.request(request)

if response.is_a?(Net::HTTPSuccess)
  puts "Queued email id: #{JSON.parse(response.body)['id']}"
else
  body = JSON.parse(response.body) rescue {}
  warn "Send failed (#{response.code}): #{body.dig('error', 'message')}"
end

Laravel (PHP)

Direct HTTP

Use Laravel's HTTP client. Bearer auth, the same JSON body, and an idempotency header for safe retries.

app/Services/WelcomeMailer.phpphp
<?php

use Illuminate\Support\Facades\Http;

$response = Http::withToken(env('MAILFULLY_API_KEY'))
    ->withHeaders([
        // An idempotency key makes the send safe to retry: a replay
        // returns the ORIGINAL { id } instead of sending a duplicate.
        'Idempotency-Key' => 'welcome:' . $user->id,
    ])
    ->post('https://api.mailfully.com/v1/emails', [
        'from' => '[email protected]',
        'to' => '[email protected]',
        'subject' => 'Welcome aboard',
        'html' => '<p>Thanks for signing up!</p>',
    ]);

if ($response->successful()) {
    logger()->info('Queued email id: ' . $response->json('id'));
} else {
    logger()->error(
        'Send failed (' . $response->status() . '): '
        . $response->json('error.message')
    );
}

Before you send

Need a key, a verified domain, or a no-domain test send first? The zero-to-send walkthrough covers all three, and the API reference documents authentication, idempotency, errors, pagination, and test mode.