ChaindocLabs

Last updated: April 28, 2026

Chaindoc SDKs

Chaindoc has two TypeScript SDKs: the Server SDK for backend work (Node.js) and the Embed SDK for dropping a signing UI into your web app. Both have full type definitions, zero runtime dependencies, and work with any framework.

Available SDKs

For npm setup and framework-specific configuration (env variables, providers, etc.), see the installation guide. Learn more in the RFC 9110 — HTTP Semantics.

  • Server SDK (@chaindoc_io/server-sdk). backend integration for Node.js 18+
  • Embed SDK (@chaindoc_io/embed-sdk). iframe-based signing UI for web applications

The Server SDK wraps the public REST API (/api/v1/*). embedded iframe endpoints are handled internally by the Embed SDK and are not part of the Server SDK surface. Learn more in the npm registry.

Server SDK

The Server SDK wraps the REST API in a type-safe Node.js interface. You'll use it to manage documents, create signature requests, handle file uploads, and trigger blockchain verification.

Installation

npm install @chaindoc_io/server-sdk

Requirements

Node.js 18 or higher (SDK uses native fetch, File, Blob, FormData)

Secret API key with sk_ prefix (create in Settings → API Access in your dashboard)

Quick start

server.ts
import { Chaindoc } from '@chaindoc_io/server-sdk';
import { readFile } from 'fs/promises';

// 1. Initialize the SDK
const chaindoc = new Chaindoc({
  secretKey: process.env.CHAINDOC_SECRET_KEY!,
  environment: 'production', // or 'staging' / 'development'
});

// 2. Upload a document file
const buffer = await readFile('./contract.pdf');
const file = new Blob([buffer], { type: 'application/pdf' });
const { media } = await chaindoc.media.upload([file]);

// 3. Create a document
const doc = await chaindoc.documents.create({
  name: 'Service Agreement',
  description: 'Contract for consulting services',
  media: media[0],
  status: 'published', // Triggers blockchain verification
  hashtags: ['#contract', '#2026'],
  meta: [{ key: 'client', value: 'Acme Corp' }],
});

// 4. Create a signature request
const sigRequest = await chaindoc.signatures.createRequest({
  versionId: doc.document.currentVersion!.id,
  recipients: [{ email: 'signer@example.com' }],
  deadline: new Date('2026-12-31'),
  embeddedFlow: true,
});

// 5. Create embedded session for the signer's iframe
const session = await chaindoc.embedded.createSession({
  email: 'signer@example.com',
  metadata: {
    documentId: doc.documentId,
    signatureRequestId: sigRequest.requestId,
  },
});

console.log('Session ID:', session.sessionId);

Express.js integration

server.ts
import express from 'express';
import { Chaindoc, ChaindocError } from '@chaindoc_io/server-sdk';
import multer from 'multer';

const app = express();
const upload = multer({ storage: multer.memoryStorage() });

const chaindoc = new Chaindoc({
  secretKey: process.env.CHAINDOC_SECRET_KEY!,
});

// Upload and create document
app.post('/api/documents', upload.single('file'), async (req, res) => {
  try {
    const file = new Blob([req.file!.buffer], { type: req.file!.mimetype });
    const { media } = await chaindoc.media.upload([file]);

    const doc = await chaindoc.documents.create({
      name: req.body.name,
      description: req.body.description || '',
      media: media[0],
      status: 'published',
      hashtags: req.body.hashtags || [],
      meta: req.body.meta || [],
    });

    res.json({ documentId: doc.documentId });
  } catch (error) {
    if (error instanceof ChaindocError) {
      res.status(error.statusCode || 500).json({ error: error.message });
    } else {
      res.status(500).json({ error: 'Internal server error' });
    }
  }
});

// Create embedded session for signer
app.post('/api/signing/session', async (req, res) => {
  try {
    const { email, documentId, signatureRequestId } = req.body;

    const session = await chaindoc.embedded.createSession({
      email,
      metadata: { documentId, signatureRequestId },
    });

    res.json({ sessionId: session.sessionId });
  } catch (error) {
    if (error instanceof ChaindocError) {
      res.status(error.statusCode || 500).json({ error: error.message });
    }
  }
});

app.listen(3000);

Next.js API Routes

app/api/signing/create-session/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { Chaindoc, ChaindocError } from '@chaindoc_io/server-sdk';

const chaindoc = new Chaindoc({
  secretKey: process.env.CHAINDOC_SECRET_KEY!,
});

export async function POST(request: NextRequest) {
  try {
    const { email, documentId, signatureRequestId } = await request.json();

    const session = await chaindoc.embedded.createSession({
      email,
      metadata: { documentId, signatureRequestId },
    });

    return NextResponse.json({
      sessionId: session.sessionId,
      expiresAt: session.expiresAt,
    });
  } catch (error) {
    if (error instanceof ChaindocError) {
      return NextResponse.json(
        { error: error.message },
        { status: error.statusCode || 500 }
      );
    }
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Module reference

The Server SDK exposes namespaced methods that mirror the REST endpoints. Below is a quick tour. see the API docs for full payload shapes.

Media. file uploads

typescript
// Upload one or more files to temporary storage
const file = new Blob([buffer], { type: 'application/pdf' });
const { media } = await chaindoc.media.upload([file]);

Documents. create, update, verify

typescript
// Create a document (triggers blockchain anchoring when status === 'published')
const { documentId, document } = await chaindoc.documents.create({
  name: 'Service Agreement',
  description: 'Q2 retainer',
  media: media[0],
  status: 'published',
  hashtags: ['#contract'],
  meta: [],
});

// Create a new version of an existing document
await chaindoc.documents.update(documentId, { /* ...same shape as create */ });

// Update access rights (who can read / edit / sign)
await chaindoc.documents.updateRights(documentId, {
  accessType: 'restricted',
  accessEmails: [{ email: 'viewer@example.com', level: 'read' }],
});

// Blockchain verification by hashes (read-only; pk_ or sk_ key is required)
const verification = await chaindoc.documents.verify({
  versionHash: '0x123...',
  certificateHash: '0x456...',
});

// Fetch blockchain verification status for a version
const status = await chaindoc.documents.getVerificationStatus(versionId);

Signatures. request and sign

typescript
// Send a document for signing
const { requestId, signatureRequest } = await chaindoc.signatures.createRequest({
  versionId: document.currentVersion!.id,
  recipients: [{ email: 'signer@example.com', signingMethod: 'embedded' }],
  deadline: new Date('2026-12-31'),
  embeddedFlow: true,
  isKycRequired: false,
});

// Poll the request status
const status = await chaindoc.signatures.getRequestStatus(requestId);

// Sign on behalf of the API key owner (when the key owner is a signer)
await chaindoc.signatures.sign({
  requestId,
  signatureId: 1,
  messageText: 'I agree',
});

// Paginated lists of the key owner's requests and signatures
const requests = await chaindoc.signatures.getMyRequests({ pageNumber: 1, pageSize: 10 });
const signatures = await chaindoc.signatures.getSignatures({ pageNumber: 1, pageSize: 10 });

Embedded. iframe sessions

typescript
// Create a 10-minute signing session (emails an OTP to the signer)
const session = await chaindoc.embedded.createSession({
  email: 'signer@example.com',
  metadata: {
    documentId,
    signatureRequestId: requestId,
    returnUrl: 'https://yourapp.com/post-sign',
  },
});
// Pass session.sessionId to the Embed SDK on your frontend.

Contracts. lifecycle management

typescript
// Create a contract in DRAFT
const { contractId, contract } = await chaindoc.contracts.create({
  documentId,
  title: 'Consulting Agreement',
  contragent: { email: 'client@example.com', name: 'Acme Corp' },
  currencyCode: 'USD',
  terminationType: 'mutual_approval',
  noticePeriodDays: 30,
  paymentTerms: [
    { type: 'recurring', name: 'Monthly retainer', amount: '5000.00', frequency: 'monthly', dayOfPeriod: 1, autoCharge: true },
  ],
});

// Send for signing
await chaindoc.contracts.send(contractId, {
  messageToSigners: 'Please sign by Friday',
  deadline: new Date('2026-05-15'),
});

// Query state
const { status, signingStatus, paymentSummary } = await chaindoc.contracts.getStatus(contractId);
const { contracts, pagination } = await chaindoc.contracts.list({ page: 1, status: 'active' });
const { activities } = await chaindoc.contracts.getActivities(contractId);

// State transitions
await chaindoc.contracts.addPaymentSetup(contractId, { /* PaymentSetupParams */ });
await chaindoc.contracts.cancel(contractId);         // DRAFT -> rejected
await chaindoc.contracts.terminate(contractId, { reason: 'Scope changed' });

Invoices. billing

typescript
// Create and (optionally) immediately send + auto-charge
const { invoiceId, invoice } = await chaindoc.invoices.create(contractId, {
  title: 'May 2026 milestone',
  amount: '2500.00',
  currencyCode: 'USD',
  dueDate: '2026-06-01T00:00:00Z',
  lineItems: [{ description: 'Dashboard build', quantity: 1, unitPrice: '2500.00' }],
  sendImmediately: true,
  autoCharge: true,
});

// Send / charge / mark as paid
await chaindoc.invoices.send(contractId, invoiceId, { autoCharge: true });
await chaindoc.invoices.charge(contractId, invoiceId);
await chaindoc.invoices.markPaid(contractId, invoiceId, {
  note: 'Wire transfer received',
  paidAt: '2026-05-10T00:00:00Z',
});

// Fetch
const invoices = await chaindoc.invoices.list(contractId, { status: 'unpaid' });
const single = await chaindoc.invoices.get(contractId, invoiceId);

Transactions. payment inspection

typescript
// All transactions for a contract's invoices
const { transactions } = await chaindoc.transactions.listByContract(contractId);

// Single transaction detail
const { transaction } = await chaindoc.transactions.get(transactionUuid);
// transaction.status: 'INITIALISED' | 'PENDING' | 'SUCCESS' | 'FAILED' | 'CANCELLED'
// transaction.stripeFee, transaction.platformFee, transaction.netAmount. string decimals

Templates. render from a saved template

typescript
// Render a template into a draft document
const { documentId, document } = await chaindoc.templates.createDocument(templateId, {
  documentName: 'NDA. Acme Corp',
  variables: { company_name: 'Acme Corp', effective_date: '2026-05-01' },
});

// Render + create signature request in one call (auto-creates the document)
const { requestId, signatureRequest } = await chaindoc.templates.sendForSigning(templateId, {
  documentName: 'NDA. Acme Corp',
  variables: { company_name: 'Acme Corp' },
  slotAssignments: [
    { signerKey: 'client', email: 'client@example.com', signingMethod: 'embedded' },
  ],
  deadline: new Date('2026-05-15'),
});

// Render a contract template with contragent info + payment terms
const { contractId } = await chaindoc.templates.createContract(templateId, {
  variables: { project_name: 'Dashboard Rebuild' },
  slotAssignments: [
    { signerKey: 'business', role: 'business' },
    { signerKey: 'client', role: 'contragent' },
  ],
  title: 'Dashboard Rebuild. SOW',
  contragent: { email: 'client@example.com' },
  deadline: new Date('2026-04-30'),
});

Webhook verification

When your backend receives a Chaindoc webhook, verify its HMAC signature before trusting the payload. The Server SDK exposes a static Chaindoc.webhooks.verify() helper. it handles the HMAC-SHA256 check and timestamp window against replay attacks, and returns the parsed event envelope on success.

webhooks.ts
import { Chaindoc } from '@chaindoc_io/server-sdk';
import express from 'express';

const app = express();

app.post('/webhooks/chaindoc',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.header('x-chaindoc-signature')!;
    const timestamp = req.header('x-chaindoc-timestamp')!;
    const rawBody = req.body.toString('utf8');

    const result = Chaindoc.webhooks.verify(
      rawBody,
      signature,
      timestamp,
      process.env.CHAINDOC_WEBHOOK_SECRET!,
    );

    if (!result.valid || !result.envelope) {
      return res.status(401).send('Invalid signature');
    }

    // result.envelope is the parsed envelope: { id, type, createdAt, data }
    const { type, data } = result.envelope;
    // ... route to your handler per event type ...

    res.status(200).end();
  },
);

Raw body is required

HMAC is computed over the raw request body: use express.raw() (or your framework's equivalent) on the webhook route, otherwise JSON parsing will mutate the bytes and break signature verification.

Key info and health checks

typescript
// Get metadata about the API key currently in use
const info = await chaindoc.getApiKeyInfo();
// { keyId, keyName, isActive, accessLevel: 'read' | 'write', lastUsedAt }

// Lightweight reachability + key validity probe
const health = await chaindoc.healthCheck();
// { status: 'ok', timestamp, apiKeyValid: true }

Retry configuration

The SDK automatically retries transient failures (5xx responses, 429 rate limits, and network errors like ECONNRESET/ETIMEDOUT) with exponential backoff plus jitter. 4xx client errors are never retried. Override defaults via the retry option:

typescript
const chaindoc = new Chaindoc({
  secretKey: process.env.CHAINDOC_SECRET_KEY!,
  timeout: 30_000,                // ms; default: 30 s
  retry: {
    maxRetries: 3,                // default
    baseDelayMs: 1000,            // starting backoff delay
    maxDelayMs: 10_000,           // ceiling. backoff won't exceed this
  },
});

Error handling

All SDK errors are instances of ChaindocError. They carry the HTTP statusCode (when the error came from the API), the raw response body, and an isRetryable flag indicating whether the SDK already exhausted its automatic retries. useful if you want to roll your own retry-on-top-of-retries logic.

typescript
import { ChaindocError } from '@chaindoc_io/server-sdk';

try {
  const doc = await chaindoc.documents.create({ /* ... */ });
} catch (error) {
  if (!(error instanceof ChaindocError)) throw error;

  console.error('Chaindoc error:', error.message, error.statusCode, error.isRetryable);

  switch (error.statusCode) {
    case 400: // Validation. error.response contains { message: string[] }
      break;
    case 401: // Invalid or missing API key
      break;
    case 403: // Insufficient permissions (e.g. read-only key on a write endpoint)
      break;
    case 404: // Not found
      break;
    case 429: // Rate limit. SDK already retried; consider queuing the operation
      break;
    default:
      // 5xx. SDK already exhausted its retries
      break;
  }
}

Embed SDK

The Embed SDK lets you show the Chaindoc signing interface inside your web app. It handles the iframe, OTP verification, and communication between your app and Chaindoc. Your users sign documents without leaving your site.

Installation

npm install @chaindoc_io/embed-sdk

What's included

Zero runtime dependencies

Full TypeScript support

Framework-agnostic (React, Vue, Angular, Svelte, plain JS)

Iframe sandboxing with origin validation

14 locales supported (see the language option below)

`publicKey` is validation-only

The publicKey (pk_...) you pass to new ChaindocEmbed({ ... }) is validated for prefix and kept on the client, but it is not sent to the iframe and not used for runtime authentication. The real auth for each signing session is the sessionId. your backend creates it via POST /api/v1/embedded/sessions (with your sk_ secret key), the signer proves they own the email via OTP, and the iframe gets a short-lived JWT for the /api/v1/embedded/* endpoints. Treat publicKey as a safe public identifier, not as a credential.

Basic usage

typescript
import { ChaindocEmbed } from '@chaindoc_io/embed-sdk';

// 1. Initialize SDK (once per page)
const chaindoc = new ChaindocEmbed({
  publicKey: process.env.NEXT_PUBLIC_CHAINDOC_PUBLIC_KEY!,
  environment: 'production', // or 'staging' / 'development'
  debug: false,
});

// 2. Get session from your backend
const response = await fetch('/api/signing/create-session', {
  method: 'POST',
  body: JSON.stringify({ signatureRequestId, signerEmail }),
});
const { sessionId } = await response.json();

// 3. Open signing flow
const instance = chaindoc.openSignatureFlow({
  sessionId,
  theme: 'light',      // 'light' | 'dark'
  language: 'en',

  onReady: () => {
    console.log('Signing interface loaded');
  },

  onSuccess: (data) => {
    console.log('Document signed:', data.signatureId);
    instance.close();
  },

  onError: (error) => {
    console.error('Signing failed:', error.code, error.message);
  },

  onCancel: () => {
    console.log('User cancelled');
    instance.close();
  },
});

Configuration reference

ChaindocEmbed is initialised once per page with the following options:

typescript
new ChaindocEmbed({
  publicKey: string,                // required; must start with 'pk_'
  environment?: 'production' | 'staging' | 'development', // default: 'production'
  debug?: boolean,                  // default: false; enables console logs
});

openSignatureFlow(options) opens a signing session and returns an EmbedInstance. Available options:

typescript
sdk.openSignatureFlow({
  // Required
  sessionId: string,                // 'ses_...' from POST /embedded/sessions

  // Appearance
  theme?: 'light' | 'dark',         // default: 'light'
  language?: 'en' | 'ru' | 'uk' | 'pl' | 'de' | 'es' | 'pt' | 'fr' | 'et' | 'kk' | 'zh' | 'hi' | 'tr' | 'vi', // default: 'en'

  // Display mode
  mode?: 'modal' | 'inline',        // default: 'modal'
  container?: HTMLElement | string, // required when mode === 'inline'

  // Modal-only
  modalWidth?: number,              // px; default: 800
  modalHeight?: number,             // px; default: 600
  zIndex?: number,                  // default: 999999
  closeOnClickOutside?: boolean,    // default: true
  closeOnEscape?: boolean,          // default: true

  // Convenience
  email?: string,                   // pre-fills the OTP form

  // Callbacks (all optional). see below
  onReady, onSuccess, onError, onCancel, onClose, onResendOtp,
});

Callbacks

typescript
{
  // Iframe loaded and ready for input
  onReady?: () => void;

  // Signer completed signing successfully
  onSuccess?: (data: {
    sessionId: string;
    documentId: string;
    signatureId: string;
    signedAt: string;
  }) => void;

  // Error inside the flow (OTP failure, expired session, network, etc.)
  onError?: (error: { code: string; message: string; details?: unknown }) => void;

  // User explicitly cancelled the flow
  onCancel?: () => void;

  // Flow was closed for any reason (success, cancel, error, programmatic close)
  onClose?: () => void;

  // Signer requested a new OTP
  onResendOtp?: (data: { email: string; sentAt: string }) => void;
}

onCancel vs onClose

onCancel fires only when the signer *explicitly* dismisses the flow (clicked close, pressed Esc, clicked outside). onClose fires on *any* teardown including after a successful onSuccess. use it for cleanup that must run regardless of outcome.

Instance methods

typescript
const instance = sdk.openSignatureFlow({ ... });

instance.isReady();              // boolean. iframe has fired onReady
instance.getSessionId();         // the sessionId this instance was opened with
instance.changeTheme('dark');    // swap theme at runtime via postMessage
instance.close();                // close the current flow

sdk.destroy();                   // close all open instances and remove listeners

ChaindocEmbed.version;           // static. current SDK version string

React integration

components/SignButton.tsx
import { useCallback, useRef, useEffect } from 'react';
import { ChaindocEmbed, EmbedInstance } from '@chaindoc_io/embed-sdk';

function SignButton({ sessionId }: { sessionId: string }) {
  const sdkRef = useRef<ChaindocEmbed | null>(null);
  const instanceRef = useRef<EmbedInstance | null>(null);

  useEffect(() => {
    sdkRef.current = new ChaindocEmbed({
      publicKey: process.env.REACT_APP_CHAINDOC_PUBLIC_KEY!,
    });

    return () => {
      sdkRef.current?.destroy();
    };
  }, []);

  const handleSign = useCallback(() => {
    if (!sdkRef.current) return;

    instanceRef.current = sdkRef.current.openSignatureFlow({
      sessionId,
      onSuccess: (data) => {
        console.log('Signed!', data.signatureId);
        instanceRef.current?.close();
      },
      onCancel: () => {
        instanceRef.current?.close();
      },
    });
  }, [sessionId]);

  return <button onClick={handleSign}>Sign Document</button>;
}

Vue 3 integration

SignButton.vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { ChaindocEmbed, type EmbedInstance } from '@chaindoc_io/embed-sdk';

const props = defineProps<{ sessionId: string }>();

let sdk: ChaindocEmbed | null = null;
let instance: EmbedInstance | null = null;

onMounted(() => {
  sdk = new ChaindocEmbed({
    publicKey: import.meta.env.VITE_CHAINDOC_PUBLIC_KEY,
  });
});

onUnmounted(() => {
  sdk?.destroy();
});

function openSignature() {
  if (!sdk) return;
  
  instance = sdk.openSignatureFlow({
    sessionId: props.sessionId,
    onSuccess: (data) => {
      console.log('Signed!', data.signatureId);
      instance?.close();
    },
    onCancel: () => {
      instance?.close();
    },
  });
}
</script>

<template>
  <button @click="openSignature">Sign Document</button>
</template>

Inline mode

Instead of a modal, embed the signing interface directly into your page:

typescript
const instance = chaindoc.openSignatureFlow({
  sessionId,
  mode: 'inline',
  container: document.getElementById('signature-container'),
  
  onSuccess: (data) => {
    console.log('Signed!');
  },
});

Theming

Customize the appearance with light or dark themes:

typescript
const instance = chaindoc.openSignatureFlow({
  sessionId,
  theme: 'dark',
  // ... other options
});

// Change theme dynamically
instance.changeTheme('light');

Full workflow example

This puts both SDKs together: Server SDK on the backend to create documents and sessions, Embed SDK on the frontend to show the signing UI.

  1. 1
    Backend: Upload DocumentUse Server SDK to upload file and create document
  2. 2
    Backend: Create Signature RequestCreate signature request with embedded flow enabled
  3. 3
    Backend: Generate SessionCreate embedded session for each signer
  4. 4
    Frontend: Initialize Embed SDKInitialize SDK with public key
  5. 5
    Frontend: Open Signing FlowOpen signing interface with session ID
  6. 6
    Frontend: Handle SuccessProcess signed document and update UI
// server.ts
import { Chaindoc } from '@chaindoc_io/server-sdk';

const chaindoc = new Chaindoc({
  secretKey: process.env.CHAINDOC_SECRET_KEY!,
});

// Upload & create document
const { media } = await chaindoc.media.upload([pdfFile]);
const doc = await chaindoc.documents.create({
  name: 'Contract',
  description: 'Service agreement',
  media: media[0],
  status: 'published',
  hashtags: ['#contract'],
  meta: [],
});

// Create signature request
const sigRequest = await chaindoc.signatures.createRequest({
  versionId: doc.document.currentVersion!.id,
  recipients: [{ email: 'signer@example.com' }],
  deadline: new Date('2026-12-31'),
  embeddedFlow: true,
});

// Create session
const session = await chaindoc.embedded.createSession({
  email: 'signer@example.com',
  metadata: {
    documentId: doc.documentId,
    signatureRequestId: sigRequest.requestId,
  },
});

// Return sessionId to frontend
res.json({ sessionId: session.sessionId });

Best practices

  • Initialize SDK once per page/component lifecycle
  • Always destroy SDK instance on component unmount
  • Handle all callback events (onSuccess, onError, onCancel)
  • Store API keys in environment variables
  • Use TypeScript for better type safety
  • Implement proper error handling with ChaindocError
  • Test with the staging environment before production deployment

Environment configuration

Both SDKs accept an environment option that selects the Chaindoc endpoint to connect to: production (default), staging, or development. You can pair production API keys with a non-production environment during QA. keys are environment-agnostic.

typescript
// Backend
const chaindoc = new Chaindoc({
  secretKey: process.env.CHAINDOC_SECRET_KEY!,
  environment: process.env.NODE_ENV === 'production' ? 'production' : 'staging',
});

// Frontend
const embed = new ChaindocEmbed({
  publicKey: process.env.NEXT_PUBLIC_CHAINDOC_PUBLIC_KEY!,
  environment: process.env.NEXT_PUBLIC_NODE_ENV === 'production' ? 'production' : 'staging',
});

What to do next

Frequently asked questions

Quick answers to the questions developers ask most often.

What languages are supported?
Official SDKs are available for JavaScript/TypeScript (Node.js and browser) and Python. The REST API can be used from any language that supports HTTP requests.
How do I install the JavaScript SDK?
Run npm install @chaindoc/sdk or yarn add @chaindoc/sdk. The package supports both CommonJS and ESM imports.
How do I install the Python SDK?
Run pip install chaindoc-sdk. The package requires Python 3.9 or higher.
Where is the full SDK documentation?
Reference documentation is available at /sdks. Auto-generated API docs are published alongside each SDK release.
Can I contribute to the SDK?
Community contributions are welcome. Open a pull request on GitHub. All contributions require a signed CLA and pass the test suite.