ChaindocLabs

Last updated: April 28, 2026

API integration

Chaindoc's REST API lets you automate document workflows, collect signatures, issue invoices against contracts, and react to events in real time. This page is the high-level tour. for the full endpoint reference see API documentation, for the SDKs see SDKs.

What you can do with the API

The public API exposes everything the web app does: upload media and create documents, send signature requests, run the full contract lifecycle (create → send → sign → invoice → terminate), render templates, manage teams, and receive webhooks for state changes. Learn more in the RFC 9110 — HTTP Semantics.

Generate API keys from the dashboard: Settings → API Keys → Create Key. You pick the access level when creating the key. a pk_-prefixed Public key for read-only use and an sk_-prefixed Secret key for full read/write. Learn more in the RFC 6749 — OAuth 2.0.

Three ways to integrate

Pick the one that fits your stack. Most production apps use the Server SDK on the backend plus the Embed SDK on the frontend.

  • REST API: direct HTTP calls, works with any language. Maximum flexibility.
  • Server SDK (@chaindoc_io/server-sdk). typed Node.js client with automatic retries, exponential backoff, and typed error classes.
  • Embed SDK (@chaindoc_io/embed-sdk). framework-agnostic browser SDK that drops the signing UI into your app as a modal or inline iframe. Works with React, Vue, Angular, Svelte, and plain JS.

If you haven't installed the SDKs yet, the installation guide covers setup for Node.js and the browser.

Quick example

Here's the same "create a document" call in three flavors. The SDK version is the most concise; the raw REST call shows exactly what's on the wire.

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

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

const buffer = await readFile('contract.pdf');
const blob = new Blob([buffer], { type: 'application/pdf' });

const { media } = await chaindoc.media.upload([blob]);

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

Authentication

Every request needs an API key in the Authorization header:

http
Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

There are two key types, distinguished by the prefix:

  • Public key (pk_). read-only. Safe for frontend code.
  • Secret key (sk_). full read/write. Backend only, never expose in client-side code.

Point the SDK (or your HTTP client) at staging for development and production for live traffic. The two environments are fully isolated. keys from one don't work in the other. See Installation for how to configure the environment option.

Rate limits

Every API key is limited to 10 requests per 60 seconds across all public endpoints combined. When you hit the limit you'll get a 429 Too Many Requests. back off and retry. The response headers tell you where you stand:

http
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 8
X-RateLimit-Reset: 1778284800

The Server SDK retries automatically with exponential backoff + jitter on 429 and 5xx responses. If you're using raw HTTP, implement retry logic yourself or switch to the SDK.

Error handling

Errors are JSON. Validation errors use an array of messages in the message field:

json
{
  "statusCode": 400,
  "message": [
    "name must be a string",
    "media should not be empty"
  ],
  "error": "Bad Request"
}

Non-validation errors return message as a single string. Common status codes: 400 (bad request), 401 (missing or invalid key), 403 (key lacks required access level), 404 (not found), 429 (rate limited), 5xx (server error. retry with backoff). The Server SDK wraps these in a ChaindocError class with statusCode, details, and an isRetryable flag so you can branch cleanly.

Common integration patterns

Full signing flow (embedded)

The most common pattern: upload a document, create a signature request, and render the signing UI inside your own app with the Embed SDK.

  1. 1
    Upload the filePOST /api/v1/media/upload (multipart field media). PDFs, Office docs, images, and supported media files up to 250 MB per file.
  2. 2
    Create the documentPOST /api/v1/documents with the media object returned from step 1. status: 'published' makes the document eligible for signing.
  3. 3
    Create a signature requestPOST /api/v1/signatures/requests with embeddedFlow: true, then create a separate embedded session for each signer. All recipients are invited in parallel. Chaindoc doesn't support sequential or conditional signing orders.
  4. 4
    Create an embedded session per signerPOST /api/v1/embedded/sessions. you get back a sessionId for each signer.
  5. 5
    Open the signing UIPass sessionId to the Embed SDK's openSignatureFlow() method. The iframe handles OTP, KYC (if required), and the signature pad.
  6. 6
    React to completionListen for signature.request.completed or poll GET /api/v1/signatures/requests/:requestId/status until status is completed. Use signature.request.rejected for declined requests.

The quick start guide walks through this flow end to end in code.

When does blockchain anchoring happen?

Publishing a document starts verification for that document version. When the last signer completes a signature request, Chaindoc emits signature.request.completed and document.signed, then a separate background job writes the signed proof to SKALE Calypso. The transaction hash and chain ID are returned on the document's verification record, and document.verified fires after on-chain confirmation.

Email-based signing (no embed)

If signers don't live inside your app, leave embeddedFlow off (it defaults to false). Chaindoc emails each recipient a secure signing link. You still get the same webhook events when they sign.

typescript
const sigRequest = await chaindoc.signatures.createRequest({
  versionId: documentVersionId,
  recipients: [
    { email: 'counterparty@example.com' },
    { email: 'legal@yourcompany.com' },
  ],
  deadline: new Date('2026-12-31'),
  embeddedFlow: false,
  message: 'Please review and sign this agreement.',
});

Contracts with invoices

For paid work you can model the full contract lifecycle instead of standalone signature requests: create a contract with payment terms, send it for signing, then issue invoices and track transactions against it.

typescript
// Create a contract with one-time payment terms
const { contractId } = await chaindoc.contracts.create({
  documentId,
  title: 'Website redesign',
  description: 'Delivery by Q3',
  contragent: { email: 'client@example.com', name: 'Acme Inc.' },
  currencyCode: 'USD',
  paymentTerms: [
    {
      type: 'one_time',
      name: 'Project fee',
      amount: '5000.00',
      dueDate: '2026-09-30',
    },
  ],
});

// Send it for signing (both sides sign in parallel)
await chaindoc.contracts.send(contractId);

// Once signed, create an invoice against it
const { invoiceId } = await chaindoc.invoices.create(contractId, {
  title: 'Website redesign invoice',
  amount: '5000.00',
  currencyCode: 'USD',
  dueDate: '2026-09-30T00:00:00Z',
});

await chaindoc.invoices.send(contractId, invoiceId);

Contracts support a single counterparty (the caller's business entity is the other side) so there are exactly two signers. See the contracts endpoints reference for the full set of operations (cancel, terminate, activities, transactions).

Identity verification (KYC)

For workflows that need identity verification, set isKycRequired: true on the signature request. Signers will complete a Sumsub-powered KYC step inside the embedded flow before they can sign. KYC happens entirely in the browser. there is no server-side KYC endpoint to call from your backend.

typescript
const sigRequest = await chaindoc.signatures.createRequest({
  versionId: documentVersionId,
  recipients: [{ email: 'signer@example.com' }],
  deadline: new Date('2026-12-31'),
  embeddedFlow: true,
  isKycRequired: true,
});

Bulk document processing

For batch imports, loop through your files and upload each one. Stay under the 10-requests-per-60-seconds limit. the SDK's built-in retries will smooth short spikes, but a steady firehose of hundreds of documents needs its own pacing.

typescript
const files = ['doc1.pdf', 'doc2.pdf', 'doc3.pdf'];

for (const file of files) {
  const buffer = await readFile(file);
  const blob = new Blob([buffer], { type: 'application/pdf' });

  const { media } = await chaindoc.media.upload([blob]);

  await chaindoc.documents.create({
    name: file,
    description: 'Batch import',
    media: media[0],
    status: 'published',
    hashtags: ['#batch'],
    meta: [{ key: 'batch', value: 'import-2026-04' }],
  });
}

Access control via API

You can set document permissions programmatically. Restrict access to specific emails or team roles:

typescript
await chaindoc.documents.updateRights(documentId, {
  accessType: 'restricted',
  accessEmails: [
    { email: 'viewer@company.com', level: 'read' },
    { email: 'editor@company.com', level: 'write' },
  ],
  accessRoles: [
    { roleId: 1, level: 'read' },
  ],
});

Best practices

  • Keep API keys in environment variables. Never commit them to version control.
  • Use the staging environment for development and production for live traffic. keys are not shared between environments.
  • Let the Server SDK handle retries. It already does exponential backoff + jitter on 429 / 5xx / network errors.
  • Verify webhook signatures with Chaindoc.webhooks.verify() and reject events older than 5 minutes to prevent replay.
  • Upload media first, then create documents. The documents.create call needs the media object returned from the upload step.
  • Paginate list endpoints. Don't fetch everything at once.
  • Read the security guide before going live. key rotation, IP allowlists, and webhook hardening are covered there.

Example use cases

A few real-world patterns teams build on top of the API:

  • E-commerce: auto-generate purchase agreements for high-value orders and send for signing at checkout.
  • HR onboarding: render an employment contract from a template and send to new hires on their start date.
  • Freelance and agency work: use contracts with one-time or recurring payment terms to bundle signing and invoicing into a single flow.
  • CRM integration: trigger signature requests from Salesforce or HubSpot deal records, mirror status back via webhooks.
  • Document archival: add blockchain verification to documents stored in your existing system by pushing them through documents.create with status: 'published'.

What to do next

  • API documentation. full endpoint reference with request and response examples
  • SDKs. Server SDK and Embed SDK configuration, callbacks, and typed errors
  • Webhooks. event catalogue, signature verification, and the 5-minute replay window
  • Quick start. ship your first signature flow in about 10 minutes
  • Installation. npm setup and environment configuration

Frequently asked questions

Quick answers to the questions developers ask most often.

What authentication does the API use?
The Chaindoc API uses Bearer token authentication. Include your API key in the Authorization header as Bearer YOUR_API_KEY.
Is there a rate limit?
Yes. Rate limits vary by plan. The default limit is 100 requests per minute. Enterprise plans can request higher limits.
What is the API base URL?
The base URL is https://api.chaindoc.io/api/v1. All endpoints are prefixed with this URL.
Do you provide SDKs?
Yes. Official SDKs are available for JavaScript/TypeScript and Python. Community SDKs exist for other languages.
How do I handle errors?
The API returns standard HTTP status codes and a JSON error body with code and message fields. Retry transient errors (5xx) with exponential backoff.
Can I upload files via the API?
Yes. Use the /documents endpoint with multipart/form-data. The maximum file size is 50 MB per upload.