ChaindocLabs

Last updated: April 28, 2026

Automation patterns

Chaindoc doesn't ship a visual workflow builder. what it gives you is a small set of composable primitives (templates, signature requests, contracts, invoices, webhooks, the Embed SDK) that you wire together in your own code. This page walks through four practical recipes that cover most real integrations.

This is a patterns guide, not a reference

For full endpoint and method details, see API documentation and SDKs. This page shows the glue between them. how the primitives connect to form complete flows.

Recipe 1: Template → signature request → status sync

The most common automation: generate a customised document from a template, send it for signing, and react to the result. Use this for any repeatable contract flow (NDAs, employment offers, vendor agreements) where the content changes per recipient but the structure doesn't. Learn more in the ISO/IEC 27001:2022.

  1. 1
    Author the template onceBuild the template in the dashboard with variables (clientName, effectiveDate) and pre-placed signer slots. Publish it. See templates.
  2. 2
    Render + send from your codeCall chaindoc.templates.sendForSigning() with the variable values and signer emails. Chaindoc generates the PDF and sends the signing invites.
  3. 3
    Persist the IDsStore the returned documentId and requestId in your own database, keyed by your domain entity (deal ID, application ID, whatever fits).
  4. 4
    Confirm completion on your backendPoll chaindoc.signatures.getRequestStatus(requestId) from your backend worker and mark your domain entity as signed when the request reaches completed.
typescript
import { Chaindoc } from '@chaindoc_io/server-sdk';

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

export async function sendNdaForDeal(dealId: string, counterpartyEmail: string) {
  const { documentId, requestId } = await chaindoc.templates.sendForSigning(
    process.env.NDA_TEMPLATE_ID!,
    {
      documentName: `NDA. deal ${dealId}`,
      variables: {
        clientName: await getClientName(dealId),
        effectiveDate: new Date().toISOString().slice(0, 10),
      },
      slotAssignments: [
        { signerKey: 'party_a', email: counterpartyEmail },
        { signerKey: 'party_b', email: 'legal@yourcompany.com' },
      ],
      deadline: addDays(new Date(), 14),
    },
  );

  await db.deals.update(dealId, {
    chaindocDocumentId: documentId,
    chaindocSignatureRequestId: requestId,
    status: 'nda_sent',
  });
}

And a simple status sync worker:

typescript
export async function syncNdaStatus(dealId: string) {
  const deal = await db.deals.findById(dealId);
  if (!deal?.chaindocSignatureRequestId || deal.status === 'nda_signed') return;

  const status = await chaindoc.signatures.getRequestStatus(
    deal.chaindocSignatureRequestId,
  );

  if (status.status === 'completed') {
    await db.deals.update(deal.id, {
      status: 'nda_signed',
      signedAt: new Date().toISOString(),
    });
  }
}

Recipe 2: Contract with recurring billing

For ongoing engagements (retainers, subscriptions, monthly services) use a contract with recurring payment terms. Chaindoc handles the signature request on both sides, then you issue invoices against the active contract and listen for payment events.

  1. 1
    Create the contractEither via chaindoc.contracts.create() with your own copy, or via chaindoc.templates.createContract() with a pre-built template. For recurring work, add a payment term with type: 'recurring'.
  2. 2
    Send it for signingchaindoc.contracts.send(). Both sides (business + contragent) are invited in parallel. Track contract.signed and contract.status_changed via webhooks.
  3. 3
    Issue invoices on the active contractOnce the contract reaches ACTIVE status, call chaindoc.invoices.create() for each billing period, then chaindoc.invoices.send() to email the invoice to the counterparty.
  4. 4
    Track payment eventsListen for invoice.paid, invoice.cancelled, transaction.created, and transaction.updated to mirror payment state into your billing system.
typescript
const { contractId } = await chaindoc.contracts.create({
  documentId,
  title: 'Monthly retainer. Acme',
  contragent: { email: 'finance@acme.com', name: 'Acme Inc.' },
  currencyCode: 'USD',
  paymentTerms: [
    {
      type: 'recurring',
      name: 'Monthly retainer',
      amount: '5000.00',
      frequency: 'monthly',
      startDate: '2026-05-01',
    },
  ],
});

await chaindoc.contracts.send(contractId);

// Later, after contract moves to ACTIVE:
const { invoiceId } = await chaindoc.invoices.create(contractId, {
  title: 'Retainer. May 2026',
  amount: '5000.00',
  currencyCode: 'USD',
  dueDate: '2026-05-31T00:00:00Z',
});

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

One contract = two signers, period

Chaindoc contracts model a single counterparty relationship: you (business) + them (contragent). If you need more than two signers on a document, use a standalone signature request instead of a contract. contracts enforce the two-signer shape at the API level.

Recipe 3: Embedded signing inside your web app

Keep users inside your product instead of bouncing them to email. You do the heavy lifting on your backend (create the document, create the signature request, create an embedded session per signer) and your frontend opens the signing UI in a modal or inline iframe.

  1. 1
    Backend. create the signature request with `embeddedFlow: true`This tells Chaindoc not to email invites. Create the request the same way as before, just flip the flag.
  2. 2
    Backend. create an embedded session per signerchaindoc.embedded.createSession() returns a sessionId tied to a specific signer on a specific signature request. Generate one session per signer, on demand, when they're ready to sign in your UI.
  3. 3
    Frontend. open the Embed SDK with the session IDopenSignatureFlow({ sessionId, email, onSuccess, onError, onCancel }). The SDK handles the iframe, OTP verification, and the signing UI.
  4. 4
    Backend. confirm via webhookDon't trust the browser alone. Confirm completion on the server with the signature request status endpoint, then process any delivered webhooks for related contract, invoice, or transaction state.
typescript
// Backend: POST /api/sessions/:userId/start-signing
app.post('/api/sessions/:userId/start-signing', async (req, reply) => {
  const user = await db.users.findById(req.params.userId);
  const request = await db.signatureRequests.findForUser(user.id);

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

  reply.send({ sessionId: session.sessionId });
});
typescript
// Frontend
import { ChaindocEmbed } from '@chaindoc_io/embed-sdk';

const chaindoc = new ChaindocEmbed({ publicKey: process.env.NEXT_PUBLIC_CHAINDOC_PUBLIC_KEY! });

async function handleSignClick() {
  const { sessionId } = await fetch('/api/sessions/me/start-signing', { method: 'POST' }).then((r) => r.json());

  await chaindoc.openSignatureFlow({
    sessionId,
    email: currentUser.email,
    theme: 'light',
    onSuccess: () => toast.success('Signed!'),
    onError: (err) => toast.error(err.message),
    onCancel: () => console.log('user closed the modal'),
  });
}

See SDKs for the full Embed SDK configuration and callback reference, and first signature for the UX your signers will experience.

Recipe 4: Webhook handling that doesn't break under load

Webhooks are the backbone of every Chaindoc automation. Getting them wrong (no signature verification, no idempotency, slow handlers) is the single fastest way to end up with duplicated contracts and mysterious Slack pages at 3am. Two rules cover most cases:

  • Verify and deduplicate: use Chaindoc.webhooks.verify() for the signature check (it also enforces the 5-minute replay window), and use the event's id as an idempotency key against a webhook_events table.
  • Return fast, process async: webhooks have a short delivery timeout. Acknowledge with a 2xx immediately, then do the real work on a background queue.
typescript
app.post('/webhooks/chaindoc', { config: { rawBody: true } }, async (req, reply) => {
  // 1. Verify signature + timestamp
  const result = Chaindoc.webhooks.verify(
    req.rawBody,
    req.headers['x-chaindoc-signature'] as string,
    req.headers['x-chaindoc-timestamp'] as string,
    process.env.CHAINDOC_WEBHOOK_SECRET!,
  );

  if (!result.valid || !result.envelope) {
    return reply.code(401).send({ error: 'invalid signature' });
  }

  const event = result.envelope;

  // 2. Deduplicate by event id
  const inserted = await db.webhookEvents.insertIfNew({
    id: event.id,
    type: event.type,
    receivedAt: new Date(),
  });
  if (!inserted) {
    return reply.code(200).send();
  }

  // 3. Hand off to a queue, ack immediately
  await queue.add('process-chaindoc-event', event);
  reply.code(200).send();
});

Chaindoc retries failed deliveries with exponential backoff before disabling an endpoint. so a handler that returns 500 repeatedly (because the downstream system is broken) will eventually get paused. Keep the handler cheap and let your queue worker do retries with its own policy.

Cross-cutting concerns

Rate limits

Every API key is limited to 10 requests per 60 seconds across all public endpoints. The Server SDK retries on 429 / 5xx automatically with exponential backoff, but if you're processing a batch of hundreds of documents add your own pacing on top. e.g. send no more than 8 requests per minute per worker, with multiple workers keyed on different keys if you need throughput.

Idempotency on your side

The Chaindoc public API doesn't accept idempotency keys today. Guard against double-submits on your side: before calling sendForSigning or contracts.create, check your own DB whether the domain entity (deal, application) already has a Chaindoc document ID attached. If it does, reuse it instead of creating a new one.

Staging vs production

Run the full flow end-to-end in staging before pointing production traffic at it. Staging has its own API keys, webhook secrets, and data. nothing crosses environments. Set environment: 'staging' on the SDK constructor to switch.

What to do next

  • API documentation. full endpoint reference for contracts, invoices, templates, and signatures
  • SDKs. Server SDK and Embed SDK details including configuration and error handling
  • Webhooks. all 17 event types, signature verification, and the 5-minute replay window
  • Templates. variable scopes, signer slots, and the three render modes
  • Security. key rotation, IP allowlists, and webhook hardening