Installation

npm install @withsema/sdk

API Client

Initialize

In production, prefer environment variables (SEMA_API_KEY, SEMA_BASE_URL) and omit the key (and optionally base URL) below.

import { SemaClient } from '@withsema/sdk';

const client = new SemaClient({
  apiKey: 'sk_live_...', // optional if SEMA_API_KEY is set
  baseUrl: 'https://dev-api.withsema.com', // optional, falls back to SEMA_BASE_URL
  timeout: 30000, // optional, in milliseconds
  maxRetries: 3, // optional, set to 0 to disable retries
});

You can omit apiKey when SEMA_API_KEY is set; omit baseUrl when SEMA_BASE_URL is set.

The client requires HTTPS in production: it rejects live keys (sk_live_...) with a non-HTTPS base URL and warns for other HTTP base URLs (e.g. localhost).

The client automatically retries requests on transient failures (5xx errors, network errors, rate limits) with exponential backoff. Rate-limited requests (429) respect the Retry-After header when present.

Create an Inbox

const inbox = await client.createInbox({
  name: 'Support Inbox',
  webhook_url: 'https://example.com/webhooks/sema',
  webhook_secret: 'whsec_...', // optional, auto-generated if omitted
});
console.log(inbox.id);

Upload Content

// From Buffer
const item = await client.uploadItem(
  inbox.id,
  Buffer.from('Hello, World!'),
  {
    sender_address: 'sender@example.com',
    subject: 'Test Upload',
    filename: 'hello.txt',
  }
);

// From file
import { readFileSync } from 'fs';
const file = readFileSync('document.pdf');
const item = await client.uploadItem(inbox.id, file, {
  sender_address: 'sender@example.com',
});

console.log(item.id, item.status, item.is_duplicate);

Get Item Details

const item = await client.getItem('...');
console.log(item.sender_address, item.subject, item.auth_decision);

List Items

const result = await client.listItems(inbox.id, { limit: 10 });
for (const item of result.items) {
  console.log(item.id, item.status);
}

List Inboxes

const result = await client.listInboxes({ limit: 10, offset: 0 });
console.log(result.total);
for (const inbox of result.inboxes) {
  console.log(inbox.id, inbox.name);
}

listInboxes(params?) accepts optional limit and offset for pagination and returns { inboxes, total }.

Pagination Iterators

For large result sets, use async iterators to automatically paginate:

// Iterate through all items in an inbox
for await (const item of client.iterItems(inboxId)) {
  console.log(item.id, item.status);
}

// Iterate through all inboxes
for await (const inbox of client.iterInboxes()) {
  console.log(inbox.id, inbox.name);
}

// Custom page size
for await (const item of client.iterItems(inboxId, { pageSize: 50 })) {
  process.stdout.write('.');
}

Check Deliveries

const result = await client.getItemDeliveries('...');
for (const delivery of result.deliveries) {
  console.log(delivery.status, delivery.attempt_count);
  for (const attempt of delivery.attempts) {
    console.log(`  Attempt ${attempt.attempt_number}: ${attempt.status_code}`);
  }
}

Webhook Verification

Initialize

import { WebhookVerifier } from '@withsema/sdk';

const verifier = new WebhookVerifier('whsec_...', {
  toleranceSeconds: 300, // optional, default 5 minutes
});

Verify and Parse

import { WebhookVerificationError } from '@withsema/sdk';

function handleWebhook(req: Request, res: Response) {
  try {
    const event = verifier.verify(req.body, req.headers);
  } catch (e) {
    if (e instanceof WebhookVerificationError) {
      return res.status(400).send(e.message);
    }
    throw e;
  }

  // Use webhookId as idempotency key
  if (alreadyProcessed(event.webhookId)) {
    return res.status(200).send('OK');
  }

  // Process the event
  console.log(event.payload.event_type);   // "ITEM_READY"
  console.log(event.payload.item_id);
  console.log(event.payload.inbox_id);
  console.log(event.payload.payload_mode); // "full" or "thin"

  // Access deliverable data
  console.log(event.payload.deliverable.raw_ref);
  if (event.payload.deliverable.sender) {
    console.log(event.payload.deliverable.sender.address);
  }

  markProcessed(event.webhookId);
  return res.status(200).send('OK');
}

Error Handling

import {
  SemaError,           // Base error
  SemaAPIError,        // API errors (has statusCode, responseBody)
  AuthenticationError, // 401 - Invalid API key
  NotFoundError,       // 404 - Resource not found
  RateLimitError,      // 429 - Rate limit exceeded
  WebhookVerificationError, // Signature/timestamp invalid
} from '@withsema/sdk';

try {
  const item = await client.getItem('nonexistent');
} catch (e) {
  if (e instanceof NotFoundError) {
    console.log('Item not found');
  } else if (e instanceof SemaAPIError) {
    console.log(`API error ${e.statusCode}: ${e.message}`);
  }
}

Observability Hooks

Add optional hooks for logging, tracing, or metrics:

const client = new SemaClient({
  apiKey: 'sk_live_...',
  hooks: {
    beforeRequest: (ctx) => {
      console.log(`→ ${ctx.method} ${ctx.url} (attempt ${ctx.attempt})`);
    },
    afterResponse: (ctx) => {
      console.log(`← ${ctx.status} in ${ctx.durationMs}ms`);
    },
    onError: (ctx, error) => {
      console.error(`✗ ${ctx.method} ${ctx.url}: ${error.message}`);
    },
  },
});

Hook context includes:

Field Description Available In
method HTTP method (GET, POST, etc.) All hooks
url Request URL All hooks
attempt Attempt number (1-based) All hooks
status HTTP status code afterResponse
durationMs Request duration in milliseconds afterResponse

Hooks can be sync or async. Errors thrown in hooks are silently ignored to prevent breaking SDK functionality.


Inline Images

Emails with pasted screenshots or inline images include cid: references in the HTML body. To render these images:

// Get attachments with presigned URLs
const { attachments } = await client.getItemAttachments(itemId);

// Build lookup from content_id to download URL
const cidToUrl = new Map<string, string>();
for (const att of attachments) {
  if (att.content_id && att.download_url) {
    cidToUrl.set(att.content_id, att.download_url);
  }
}

// Replace cid: references in HTML body
let html = event.payload.deliverable.content_summary?.body_html ?? '';
for (const [cid, url] of cidToUrl) {
  html = html.replace(`cid:${cid}`, url);
}

Type Reference

All responses are fully typed:

  • Inbox - Inbox configuration and status
  • Item - Item details including sender, status, metadata
  • Delivery - Delivery status and attempts
  • WebhookEvent - Verified webhook with webhookId, timestamp, payload
  • WebhookPayload - Parsed webhook body with deliverable data

Types are exported from the main package:

import type { Inbox, Item, WebhookPayload } from '@withsema/sdk';