@ulvio/client
v0.3.1
Published
Official TypeScript client for the Ulvio platform, html-to-pdf, and utilities services.
Downloads
534
Readme
@ulvio/client
Official TypeScript client for the Ulvio platform.
A single Ulvio class exposes domain-scoped sub-clients (mail, mailbox, sms, whatsapp, voice, files, ai, html-to-pdf, utilities), all reachable via the same baseUrl + apiKey. The platform proxies html-to-pdf and utilities to their internal services transparently, so consumers only need one set of credentials.
Install
npm install @ulvio/clientRequires Node.js 24+.
Configuration
import { Ulvio } from '@ulvio/client';
const client = new Ulvio({
baseUrl: process.env.ULVIO_BASE_URL,
apiKey: process.env.ULVIO_API_KEY,
});Both baseUrl and apiKey are required for every sub-client. If either is missing when a method is called, the call throws an UlvioError with code not_configured — distinct from runtime/network errors, so consumers can detect misconfiguration explicitly:
import { UlvioError, NOT_CONFIGURED_CODE } from '@ulvio/client';
try {
await client.mail.sendTransactional({ ... });
} catch (err) {
if (err instanceof UlvioError && err.code === NOT_CONFIGURED_CODE) {
// surface a deployment-time configuration error
}
throw err;
}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: { ... } });Migrating from 0.2.x to 0.3.0
The 0.3.0 release collapses the four per-service config fields to a single { baseUrl, apiKey } pair, and routes html-to-pdf and utilities through the platform forwarder.
- Replace
platformApiUrl/htmlToPdfApiUrl/utilitiesApiUrlwith onebaseUrlpointing atapi.ulvio.dev(or your platform-dev URL). - Replace
platformApiKeywithapiKey— the same key now authenticates every sub-client. - The legacy env vars (
ULVIO_PLATFORM_API_URL,HTML_TO_PDF_API_URL,UTILITIES_API_URL) should be replaced withULVIO_BASE_URL+ULVIO_API_KEY. - The three legacy error codes (
platform_not_configured,html_to_pdf_not_configured,utilities_not_configured) are gone; catchnot_configured(exported asNOT_CONFIGURED_CODE) instead.
// before (0.2.x)
new Ulvio({
platformApiUrl: process.env.ULVIO_PLATFORM_API_URL,
platformApiKey: process.env.ULVIO_PLATFORM_API_KEY,
htmlToPdfApiUrl: process.env.HTML_TO_PDF_API_URL,
utilitiesApiUrl: process.env.UTILITIES_API_URL,
});
// after (0.3.0)
new Ulvio({
baseUrl: process.env.ULVIO_BASE_URL,
apiKey: process.env.ULVIO_API_KEY,
});The platform side must be running a version that exposes /html-to-pdf/* and /utilities/* forwarders (ulvio-dev/platform and ulvio-dev/platform-dev).
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.
