unsent
unsent.dev
Guides

TanStack Start

Complete guide to using Unsent in TanStack Start applications with server functions and loaders

The Unsent SDK integrates seamlessly with TanStack Start, leveraging server functions and loaders. This guide covers everything from basic email sending to advanced features like campaigns, webhooks, and analytics.

Prerequisites

Node.js 18+: TanStack Start requires Node.js 18 or higher

Unsent API Key: Generate one in your Unsent dashboard

Verified Domain: Set up a domain in the Domains section

Installation

Install the SDK using your preferred package manager:

npm install @unsent/sdk
pnpm add @unsent/sdk
yarn add @unsent/sdk
bun add @unsent/sdk

Configuration

Add your Unsent API key to .env:

.env
UNSENT_API_KEY=un_...

TanStack Start automatically loads environment variables from .env files.

Quick Start

Create a server function to send emails:

app/functions/send-email.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const sendEmail = createServerFn('POST', async (payload: { email: string }) => {
  const { data, error } = await unsent.emails.send({
    from: 'Acme <onboarding@unsent.dev>',
    to: payload.email,
    subject: 'Welcome!',
    html: '<h1>Hello!</h1><p>Thanks for signing up.</p>',
  });

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Use in your component:

app/routes/index.tsx
import { sendEmail } from '../functions/send-email';

export default function Home() {
  const [email, setEmail] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    
    try {
      await sendEmail({ email });
      alert('Email sent successfully!');
      setEmail('');
    } catch (error) {
      alert('Failed to send email');
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Enter your email"
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Sending...' : 'Send Email'}
      </button>
    </form>
  );
}

Email Operations

Send with Attachments

app/functions/send-invoice.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const sendInvoice = createServerFn('POST', async () => {
  const { data, error } = await unsent.emails.send({
    from: 'Billing <billing@yourdomain.com>',
    to: 'customer@example.com',
    subject: 'Your Invoice',
    html: '<p>Please find your invoice attached.</p>',
    attachments: [{
      filename: 'invoice.pdf',
      content: 'base64-encoded-content',
      contentType: 'application/pdf'
    }]
  });

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Schedule Emails

app/functions/schedule-email.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const scheduleEmail = createServerFn('POST', async () => {
  const scheduledTime = new Date();
  scheduledTime.setHours(scheduledTime.getHours() + 24);
  
  const { data, error } = await unsent.emails.send({
    from: 'Reminders <reminders@yourdomain.com>',
    to: 'user@example.com',
    subject: 'Meeting Reminder',
    html: '<p>Your meeting is tomorrow at 10 AM</p>',
    scheduledAt: scheduledTime.toISOString()
  });

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Batch Sending

Send up to 100 emails in one request:

app/functions/send-batch.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

interface User {
  email: string;
  name: string;
}

export const sendBatchEmails = createServerFn('POST', async (users: User[]) => {
  const emails = users.map(user => ({
    from: 'Newsletter <news@yourdomain.com>',
    to: user.email,
    subject: 'Monthly Update',
    html: `<h1>Hi ${user.name}!</h1><p>Here's what's new...</p>`
  }));

  const { data, error } = await unsent.emails.batch(emails);

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

List Emails

app/functions/list-emails.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const listEmails = createServerFn('GET', async (page: number = 1) => {
  const { data, count, error } = await unsent.emails.list({
    page,
    limit: 50
  });

  if (error) {
    throw new Error(error.message);
  }

  return { data, count };
});

Get Email Status

app/functions/get-email.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const getEmail = createServerFn('GET', async (id: string) => {
  const { data, error } = await unsent.emails.get(id);

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Contact Management

Create Contact Book

app/functions/create-contact-book.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const createContactBook = createServerFn('POST', async (payload: { name: string; emoji: string }) => {
  const { data, error } = await unsent.contactBooks.create(payload);

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

List Contact Books

app/functions/list-contact-books.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const listContactBooks = createServerFn('GET', async () => {
  const { data, error } = await unsent.contactBooks.list();

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Manage Contacts

app/functions/create-contact.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

interface ContactPayload {
  bookId: string;
  contact: {
    email: string;
    firstName: string;
    lastName: string;
    subscribed: boolean;
    metadata?: Record<string, any>;
  };
}

export const createContact = createServerFn('POST', async (payload: ContactPayload) => {
  const { data, error } = await unsent.contacts.create(
    payload.bookId,
    payload.contact
  );

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Upsert Contact

app/functions/upsert-contact.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

interface UpsertPayload {
  bookId: string;
  contactId: string;
  contact: Record<string, any>;
}

export const upsertContact = createServerFn('POST', async (payload: UpsertPayload) => {
  const { data, error } = await unsent.contacts.upsert(
    payload.bookId,
    payload.contactId,
    payload.contact
  );

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Campaign Management

Create Campaign

app/functions/create-campaign.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

interface CampaignPayload {
  name: string;
  subject: string;
  html: string;
  bookId: string;
}

export const createCampaign = createServerFn('POST', async (payload: CampaignPayload) => {
  const { data, error } = await unsent.campaigns.create({
    name: payload.name,
    subject: payload.subject,
    html: payload.html,
    from: 'Newsletter <news@yourdomain.com>',
    contactBookId: payload.bookId,
    replyTo: 'support@yourdomain.com'
  });

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

List Campaigns

app/functions/list-campaigns.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const listCampaigns = createServerFn('GET', async () => {
  const { data, error } = await unsent.campaigns.list();

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Schedule Campaign

app/functions/schedule-campaign.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const scheduleCampaign = createServerFn('POST', async (payload: { id: string; scheduledAt: string }) => {
  const { data, error } = await unsent.campaigns.schedule(payload.id, {
    scheduledAt: payload.scheduledAt,
    batchSize: 1000,
    batchInterval: 60
  });

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Domain Management

List Domains

app/functions/list-domains.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const listDomains = createServerFn('GET', async () => {
  const { data, error } = await unsent.domains.list();

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Create Domain

app/functions/create-domain.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const createDomain = createServerFn('POST', async (payload: { name: string; region: string }) => {
  const { data, error } = await unsent.domains.create(payload);

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Verify Domain

app/functions/verify-domain.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const verifyDomain = createServerFn('POST', async (id: string) => {
  const { data, error } = await unsent.domains.verify(id);

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Templates

Create Template

app/functions/create-template.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

interface TemplatePayload {
  name: string;
  subject: string;
  html: string;
  content: string;
}

export const createTemplate = createServerFn('POST', async (payload: TemplatePayload) => {
  const { data, error } = await unsent.templates.create(payload);

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

List Templates

app/functions/list-templates.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const listTemplates = createServerFn('GET', async () => {
  const { data, error } = await unsent.templates.list();

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Send with Template

app/functions/send-template-email.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

interface SendTemplatePayload {
  to: string;
  templateId: string;
  variables: Record<string, any>;
}

export const sendTemplateEmail = createServerFn('POST', async (payload: SendTemplatePayload) => {
  const { data, error } = await unsent.emails.send({
    from: 'Hello <hello@yourdomain.com>',
    to: payload.to,
    templateId: payload.templateId,
    variables: payload.variables
  });

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Analytics

Get Account Analytics

app/functions/get-analytics.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const getAnalytics = createServerFn('GET', async () => {
  const { data, error } = await unsent.analytics.get();

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Get Time Series Data

app/functions/get-timeseries.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const getTimeSeries = createServerFn('GET', async (days: number = 30) => {
  const { data, error } = await unsent.analytics.getTimeSeries({ days });

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Get Reputation Score

app/functions/get-reputation.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const getReputation = createServerFn('GET', async () => {
  const { data, error } = await unsent.analytics.getReputation();

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Webhooks

Manage Webhooks

app/functions/list-webhooks.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const listWebhooks = createServerFn('GET', async () => {
  const { data, error } = await unsent.webhooks.list();

  if (error) {
    throw new Error(error.message);
  }

  return data;
});
app/functions/create-webhook.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const createWebhook = createServerFn('POST', async (payload: { url: string; events: string[] }) => {
  const { data, error } = await unsent.webhooks.create(payload);

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Suppression Management

Manage Suppressions

app/functions/list-suppressions.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const listSuppressions = createServerFn('GET', async () => {
  const { data, error } = await unsent.suppressions.list({
    page: 1,
    limit: 50
  });

  if (error) {
    throw new Error(error.message);
  }

  return data;
});
app/functions/add-suppression.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const addSuppression = createServerFn('POST', async (payload: { email: string; reason: string }) => {
  const { data, error } = await unsent.suppressions.add(payload);

  if (error) {
    throw new Error(error.message);
  }

  return data;
});
app/functions/remove-suppression.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const removeSuppression = createServerFn('DELETE', async (email: string) => {
  const { data, error } = await unsent.suppressions.delete(email);

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

API Key Management

Manage API Keys

app/functions/list-api-keys.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const listApiKeys = createServerFn('GET', async () => {
  const { data, error } = await unsent.apiKeys.list();

  if (error) {
    throw new Error(error.message);
  }

  return data;
});
app/functions/create-api-key.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const createApiKey = createServerFn('POST', async (payload: { name: string; permission: string }) => {
  const { data, error } = await unsent.apiKeys.create(payload);

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Using Loaders

Fetch data server-side with loaders:

app/routes/emails.tsx
import { createFileRoute } from '@tanstack/react-router';
import { listEmails } from '../functions/list-emails';

export const Route = createFileRoute('/emails')({
  loader: async () => {
    const result = await listEmails(1);
    return result;
  },
  component: EmailsPage,
});

function EmailsPage() {
  const { data, count } = Route.useLoaderData();

  return (
    <div>
      <h1>Emails ({count})</h1>
      <ul>
        {data.map((email) => (
          <li key={email.id}>
            {email.subject} - {email.status}
          </li>
        ))}
      </ul>
    </div>
  );
}

Error Handling

Comprehensive Error Handling

app/functions/send-safe-email.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const sendSafeEmail = createServerFn('POST', async (payload: { email: string }) => {
  try {
    const { data, error } = await unsent.emails.send({
      from: 'Hello <hello@yourdomain.com>',
      to: payload.email,
      subject: 'Test',
      html: '<p>Test</p>'
    });

    if (error) {
      switch (error.code) {
        case 'VALIDATION_ERROR':
          throw new Error('Invalid email data');
        case 'RATE_LIMIT_EXCEEDED':
          throw new Error(`Too many requests. Retry after ${error.retryAfter}s`);
        case 'DOMAIN_NOT_VERIFIED':
          throw new Error('Domain not verified');
        default:
          throw new Error(error.message);
      }
    }

    return data;
  } catch (err) {
    throw new Error('Failed to send email');
  }
});

Best Practices

Shared Utilities

Create shared utilities for reusability:

app/lib/unsent.ts
import { Unsent } from '@unsent/sdk';

let unsentInstance: Unsent | null = null;

export function getUnsent() {
  if (!unsentInstance) {
    if (!process.env.UNSENT_API_KEY) {
      throw new Error('UNSENT_API_KEY is not defined');
    }
    unsentInstance = new Unsent(process.env.UNSENT_API_KEY);
  }
  return unsentInstance;
}

Use in your server functions:

app/functions/example.ts
import { createServerFn } from '@tanstack/start';
import { getUnsent } from '../lib/unsent';

export const exampleFunction = createServerFn('POST', async () => {
  const unsent = getUnsent();
  // Use unsent...
});

Idempotency Keys

Use idempotency keys to prevent duplicate sends:

app/functions/signup-email.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const sendSignupEmail = createServerFn('POST', async (payload: { userId: string; email: string }) => {
  const { data, error } = await unsent.emails.send(
    {
      from: 'Welcome <hello@yourdomain.com>',
      to: payload.email,
      subject: 'Welcome!',
      html: '<p>Thanks for signing up!</p>'
    },
    {
      idempotencyKey: `signup-${payload.userId}`
    }
  );

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Type Safety

Leverage TypeScript for type safety:

app/functions/typed-send.ts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
import type { SendEmailPayload } from '@unsent/sdk';

const unsent = new Unsent(process.env.UNSENT_API_KEY);

export const sendTypedEmail = createServerFn('POST', async (payload: SendEmailPayload) => {
  const { data, error } = await unsent.emails.send(payload);

  if (error) {
    throw new Error(error.message);
  }

  return data;
});

Advanced Patterns

Form with Mutations

app/routes/newsletter.tsx
import { createFileRoute } from '@tanstack/react-router';
import { sendEmail } from '../functions/send-email';
import { useState } from 'react';

export const Route = createFileRoute('/newsletter')({
  component: NewsletterPage,
});

function NewsletterPage() {
  const [email, setEmail] = useState('');
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setStatus('loading');
    
    try {
      await sendEmail({ email });
      setStatus('success');
      setEmail('');
    } catch (error) {
      setStatus('error');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="your@email.com"
        required
      />
      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Subscribing...' : 'Subscribe'}
      </button>
      
      {status === 'success' && <p>Successfully subscribed!</p>}
      {status === 'error' && <p>Failed to subscribe. Please try again.</p>}
    </form>
  );
}

Resources