@ulvio/client
v0.1.0
Published
Official TypeScript client for the Ulvio platform, html-to-pdf, and utilities services.
Readme
@ulvio/client
Official TypeScript client for the Ulvio platform, plus the html-to-pdf and utilities services.
A single Ulvio class exposes domain-scoped sub-clients. Each backend service is configured independently and is optional — projects only need to set the URLs (and platform key) for the services they actually use.
Install
npm install @ulvio/clientRequires Node.js 24+.
Configuration
import { Ulvio } from '@ulvio/client';
const client = new Ulvio({
// Platform — required for mail / mailbox / sms / whatsapp / voice / files / ai
platformApiUrl: process.env.ULVIO_PLATFORM_API_URL,
platformApiKey: process.env.ULVIO_PLATFORM_API_KEY,
// HTML-to-PDF — required for client.htmlToPdf.*
htmlToPdfApiUrl: process.env.HTML_TO_PDF_API_URL,
// Utilities (MJML / Liquid / Markdown) — required for client.utilities.*
utilitiesApiUrl: process.env.UTILITIES_API_URL,
});| Sub-client | Requires |
|---|---|
| client.mail, client.mailbox, client.sms, client.whatsapp, client.voice, client.files, client.ai | platformApiUrl + platformApiKey |
| client.htmlToPdf | htmlToPdfApiUrl |
| client.utilities | utilitiesApiUrl |
If you call a sub-client whose required config is missing, the call throws an UlvioError with one of:
platform_not_configuredhtml_to_pdf_not_configuredutilities_not_configured
This is distinct from runtime/network errors, so consumers can detect misconfiguration explicitly:
import { UlvioError } from '@ulvio/client';
try {
await client.mail.sendTransactional({ ... });
} catch (err) {
if (err instanceof UlvioError && err.code === 'platform_not_configured') {
// surface a deployment-time configuration error
}
throw err;
}Partial configuration
A project that only needs html-to-pdf can construct the client with a single field:
const client = new Ulvio({ htmlToPdfApiUrl: 'http://html-to-pdf:3002' });
await client.htmlToPdf.convert({ html: btoa('<h1>hi</h1>'), outputMode: 'base64' });
// client.mail.* would throw platform_not_configured.Usage
await client.mail.sendTransactional({
from: '[email protected]',
to: ['[email protected]'],
subject: 'Welcome',
body_html: '<p>Hi!</p>',
});Mailbox
const { messages } = await client.mailbox.list(20);
const msg = await client.mailbox.get(messages[0].id);
await client.mailbox.markProcessed(msg.id);getConnectorStatus() reports whether the upstream connector is healthy. When unhealthy, the listing/reading methods throw an UlvioError whose code is CONNECTOR_UNHEALTHY — use the isConnectorUnhealthyError helper to detect it:
import { isConnectorUnhealthyError } from '@ulvio/client';
try {
await client.mailbox.list();
} catch (err) {
if (isConnectorUnhealthyError(err)) {
// back off polling
} else {
throw err;
}
}SMS / WhatsApp / Voice
await client.sms.send({ from: '+1...', to: '+1...', body: 'hello' });
await client.whatsapp.send({ to: '+1...', template_name: 'reminder', language_code: 'en' });
await client.voice.transcribe({ file: base64Audio, file_name: 'call.webm' });Files
await client.files.upload('reports/2026/q1.pdf', buffer, 'application/pdf');
const res = await client.files.get('reports/2026/q1.pdf');
const { url } = await client.files.presignedDownloadUrl('reports/2026/q1.pdf', 600);AI proxy
import { z } from 'zod';
const Person = z.object({ name: z.string(), age: z.number() });
const data = await client.ai.parse({
model: 'claude-opus-4-7',
input: 'Extract the person from: "Ada Lovelace, 36"',
schema: Person,
});
for await (const event of client.ai.stream({
model: 'claude-opus-4-7',
input: '...',
schema: Person,
})) {
if (event.type === 'partial') console.log(event.data);
if (event.type === 'complete') console.log('done', event.data);
}HTML-to-PDF
const { pdf } = await client.htmlToPdf.convert(
{ html: btoa('<h1>Invoice</h1>'), outputMode: 'base64' },
{
onQueued: ({ position }) => console.log('queued at', position),
onProcessing: ({ progress }) => console.log('progress', progress),
},
);Or have the service PUT the rendered PDF directly to a presigned URL:
await client.htmlToPdf.convert({
sourceUrl: 'https://example.com/invoice/1',
uploadUrl: presignedPutUrl,
});Utilities
const { html } = await client.utilities.compileMjml({ mjml: '...' });
const { result } = await client.utilities.renderLiquid({ template: 'Hi {{ name }}', data: { name: 'Ada' } });
const { variables } = await client.utilities.extractLiquidVariables({ template: '{{ a }} {{ b }}' });
const { html: md } = await client.utilities.renderMarkdown({ markdown: '# title' });
const { html: email } = await client.utilities.renderEmail({ mjml: '...', data: { ... } });Development
npm install
npm run typecheck
npm test
npm run buildEnd-to-end tests run against live containers via Docker Compose:
npm run test:e2eReleasing
Tag the commit with the version (vX.Y.Z); the Release workflow verifies the tag matches package.json and runs npm publish --provenance.
npm version patch # bumps package.json + creates a vX.Y.Z tag
git push --follow-tagsThe workflow needs an NPM_TOKEN secret with publish access to the @ulvio scope.
