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:
Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxThere 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:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 8
X-RateLimit-Reset: 1778284800The 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:
{
"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.
- 1Upload the file
POST /api/v1/media/upload(multipart fieldmedia). PDFs, Office docs, images, and supported media files up to 250 MB per file. - 2Create the document
POST /api/v1/documentswith the media object returned from step 1.status: 'published'makes the document eligible for signing. - 3Create a signature request
POST /api/v1/signatures/requestswithembeddedFlow: 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. - 4Create an embedded session per signer
POST /api/v1/embedded/sessions. you get back asessionIdfor each signer. - 5Open the signing UIPass
sessionIdto the Embed SDK'sopenSignatureFlow()method. The iframe handles OTP, KYC (if required), and the signature pad. - 6React to completionListen for
signature.request.completedor pollGET /api/v1/signatures/requests/:requestId/statusuntilstatusiscompleted. Usesignature.request.rejectedfor 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.
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.
// 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.
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.
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:
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
stagingenvironment for development andproductionfor 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.createcall 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.createwithstatus: '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.