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 snippetCall the SDK from a Route Handler or Server Action. Branch on error — the SDK never throws — and return the queued id.
// 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: "you@yourdomain.com",
to: "customer@example.com",
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 snippetA POST handler that sends with an idempotency key (safe to retry) and maps the SDK result onto the HTTP response.
// 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: "you@yourdomain.com",
to: "customer@example.com",
subject: "Welcome aboard",
html: "<p>Thanks for signing up!</p>",
replyTo: "support@yourdomain.com",
},
// 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 HTTPNo Ruby SDK ships yet, so call the API directly with the standard library. The request shape is identical to the SDK's.
# 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: "you@yourdomain.com",
to: "customer@example.com",
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')}"
endLaravel (PHP)
Direct HTTPUse Laravel's HTTP client. Bearer auth, the same JSON body, and an idempotency header for safe retries.
<?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' => 'you@yourdomain.com',
'to' => 'customer@example.com',
'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.