Step-by-step guideTypeScript · Server-side

Send your first email

Publish a template, generate your client, test in the virtual inbox, and send through your provider.

BEFORE YOU START

You’ll need a Papier workspace and a server-side TypeScript project. These scripts use Node.js 22.23.4 or later and an ESM project with "type": "module" in package.json. The examples use project acme and template welcome-email; substitute your own slugs.

01

Prepare a welcome template#

In your Papier workspace, create an email template with the slug welcome-email. Add a personalized greeting such as Welcome, {{ name }}! in the template’s HTML or text content. Set this data schema and publish a version of the template.

Template data schema
{
  "type": "object",
  "properties": {
    "name": { "type": "string" }
  },
  "required": ["name"],
  "additionalProperties": false
}

The schema makes name required. The generated client uses it for TypeScript autocomplete, and the SDK validates the data at runtime.

02

Connect and generate your client#

Run these commands from your application directory. During init, select your organization, project, and template versions. sync resolves schemas and generates the client.

Terminal
npm install @usepapier/client @usepapier/cli
npm install --save-dev tsx

npx @usepapier/cli login
npx @usepapier/cli init
npx @usepapier/cli sync

Add a Papier API key to your local .env file. Keep it on the server and out of version control.

.env
PAPIER_API_KEY=your_papier_api_key

welcome-email becomes papier.emails.welcomeEmail. If that property is missing, check the project and template selection in .papier/papier.json, then run sync again.

03

Test with the virtual inbox#

Start with virtualInboxAdapter. It renders the template and records the test send through the SDK’s logging flow without delivering an email to the recipient.

test-email.ts
import { createPapier } from '@usepapier/client';
import { virtualInboxAdapter } from '@usepapier/client/base';

const apiKey = process.env.PAPIER_API_KEY;
if (!apiKey) throw new Error('Set PAPIER_API_KEY on the server.');

const papier = createPapier({
  project: 'acme',
  apiKey,
  environment: 'development',
  emailConfig: {
    from: { name: 'Acme', email: 'hello@example.com' },
    adapters: [virtualInboxAdapter],
  },
});

const result = await papier.emails.welcomeEmail.send({
  to: 'alex@example.com',
  subject: 'Welcome aboard, Alex',
  data: { name: 'Alex' },
});

console.log(result.success, result.adapter, result.id);
// true, 'virtual-inbox', a generated test ID
Terminal
npx tsx --env-file=.env test-email.ts

You should see true, virtual-inbox, and a test ID. Open your workspace’s virtual inbox and developer logs to inspect the test. The SDK still needs access to your published template; a missing log can also mean its logging request failed.

04

Send through a provider#

Follow Configure email providers to save providers.ts and client.ts beside your script. That guide connects Resend or SendGrid, adds a verified sender, and registers the adapter in emailConfig.adapters.

Then save and run this script with your own recipient address. The SDK fetches the template, validates your data, renders HTML, and calls the configured adapter.

send-email.ts
import { papier } from './client.js';

try {
  const result = await papier.emails.welcomeEmail.send({
    to: 'alex@example.com', // Replace with your own recipient.
    subject: 'Welcome aboard, Alex',
    data: { name: 'Alex' },
  });

  console.log('Accepted by:', result.adapter, result.id);
} catch (error) {
  console.error(error instanceof Error ? error.message : 'Email failed');
  process.exitCode = 1;
}
Terminal
npx tsx --env-file=.env send-email.ts

A successful result means the adapter accepted the send. Use your provider’s delivery events to confirm that the message reached the recipient.

05

Handle failures and send later#

The try/catch above covers schema errors, template-fetch errors, and provider failures. Correct invalid data before retrying. If no adapter is registered, the SDK throws a “no email adapters configured” error.

For a queue or outbox workflow, call build() first. It validates and returns a serializable payload; your application owns persistence and scheduling. This minimal example sends the payload immediately so you can try both methods.

build-payload.ts
import { papier } from './client.js';

const payload = await papier.emails.welcomeEmail.build({
  to: 'alex@example.com',
  subject: 'Welcome aboard, Alex',
  data: { name: 'Alex' },
});

// build() validates the data. It does not send an email or render HTML.
// Persist payload in your own outbox or queue if you need to send it later.

const result = await papier.send(payload);
console.log('Accepted by:', result.adapter, result.id);

Use a worker configured for the same project to send a stored payload. The payload contains template metadata and data, rather than a snapshot of rendered HTML; rendering and template resolution happen when you send.

Keep building.